Actually support creating assets

This commit is contained in:
2025-06-08 06:10:45 +02:00
parent 1c65630565
commit 257abddc15
9 changed files with 192 additions and 11 deletions

View File

@@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.HashMap;
/**
* Controller for handling web requests related to assets.
* Provides endpoints for viewing and creating assets.
@@ -68,8 +70,13 @@ public class WebController {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public String createTypePost(Model model, @PathVariable String type, @RequestBody MultiValueMap<String, String> formData) {
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type));
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type));
return "create_asset";
var formMap = new HashMap<String, String>();
formData.forEach((key, values) -> {
if (!values.isEmpty()) {
formMap.put(key, values.getFirst());
}
});
var asset = assetService.createAsset(type, formMap);
return "redirect:/view/" + asset.getQr();
}
}

View File

@@ -1,11 +1,14 @@
package be.seeseepuff.pcinv.meta;
import be.seeseepuff.pcinv.models.Asset;
import lombok.Builder;
import lombok.Getter;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Describes the properties of an asset
@@ -26,6 +29,9 @@ public class AssetDescriptor {
@lombok.Singular
private Collection<AssetProperty> properties;
/// A supplier that can be used to create a new instance of the asset type described by this descriptor.
private Supplier<Asset> instanceProducer;
/**
* Loads the asset properties from a given asset class.
*
@@ -35,15 +41,25 @@ public class AssetDescriptor {
public static AssetDescriptor loadFrom(Class<?> assetType) {
var assetInfo = assetType.getAnnotation(AssetInfo.class);
Objects.requireNonNull(assetInfo, "Asset class must be annotated with @AssetInfo");
return AssetDescriptor.builder()
var builder = AssetDescriptor.builder()
.type(assetInfo.type())
.displayName(assetInfo.displayName())
.visible(assetInfo.isVisible())
.properties(Arrays.stream(assetType.getDeclaredFields())
.map(AssetProperty::loadFrom)
.filter(Objects::nonNull)
.toList())
.build();
.toList());
if (Asset.class.isAssignableFrom(assetType)) {
builder.instanceProducer(() -> {
try {
return (Asset) assetType.getConstructor().newInstance();
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException |
InvocationTargetException e) {
throw new RuntimeException(e);
}
});
}
return builder.build();
}
/**
@@ -78,4 +94,16 @@ public class AssetDescriptor {
public String asString(AssetProperty property) {
return String.format("%s-%s", type, property.getName());
}
/**
* Creates a new instance of the asset type described by this descriptor.
*
* @return A new instance of the asset type.
*/
public Asset newInstance() {
if (instanceProducer == null) {
throw new IllegalStateException("Instance producer is not set for asset descriptor: " + type);
}
return instanceProducer.get();
}
}

View File

@@ -13,6 +13,8 @@ public class AssetOption {
private final String value;
/// The display name of the option.
private final String displayName;
/// The actual enum value associated with this option.
private final Object enumConstant;
/// Whether this option is the default value for the property.
private final boolean isDefaultValue;
}

View File

@@ -9,6 +9,7 @@ import lombok.Singular;
import java.lang.reflect.Field;
import java.util.List;
import java.util.function.BiConsumer;
/**
* Represents a property of an asset, such as its name or type.
@@ -32,6 +33,8 @@ public class AssetProperty {
private final boolean capacityAsSI;
/// Whether the capacity should be displayed in IEC units (e.g., GiB, MiB).
private final boolean capacityAsIEC;
/// A setter function that can be used to set the value of the property on an asset.
private final BiConsumer<Object, Object> setter;
/**
* Enum representing the possible types of asset properties.
@@ -74,7 +77,15 @@ public class AssetProperty {
.name(property.getName())
.displayName(annotation.value())
.type(type)
.required(annotation.required());
.required(annotation.required())
.setter((obj, value) -> {
try {
property.setAccessible(true);
property.set(obj, value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
if (type.isEnum) {
var enumConstants = property.getType().getEnumConstants();
@@ -86,6 +97,7 @@ public class AssetProperty {
.value(assetEnum.getValue())
.displayName(assetEnum.getDisplayName())
.isDefaultValue(assetEnum.isDefaultValue())
.enumConstant(enumConstant)
.build();
builder.option(option);
}
@@ -119,6 +131,19 @@ public class AssetProperty {
}
}
/**
* Sets the value of the property on the given asset.
*
* @param asset The asset to set the property on.
* @param value The value to set for the property.
*/
public void setValue(Object asset, Object value) {
if (value == null && required) {
throw new IllegalArgumentException("Property '" + name + "' is required but received null value.");
}
setter.accept(asset, value);
}
@Override
public String toString() {
var enumOptions = "";

View File

@@ -5,5 +5,9 @@ public interface Asset {
GenericAsset getAsset();
default long getQr() {
return getAsset().getQr();
}
void setAsset(GenericAsset asset);
}

View File

@@ -31,7 +31,7 @@ public class HddAsset implements Asset
/// The capacity of the drive in bytes.
@Property("Capacity")
@Capacity(si = true, iec = true)
private long capacity;
private Long capacity;
/// The drive's interface type, such as SATA, IDE, ISA-16, ...
@Property("Interface Type")

View File

@@ -31,7 +31,7 @@ public class RamAsset implements Asset
/// The capacity of the RAM in bytes.
@Property("Capacity")
@Capacity
private long capacity;
private Long capacity;
/// The type of memory. E.g.: DDR2, SDRAM, ISA-8, etc...
@Property("Type")

View File

@@ -5,6 +5,17 @@ import org.springframework.stereotype.Repository;
@Repository
public interface AssetRepository<T extends Asset> {
T saveAndFlush(T entity);
default Asset saveAndFlushAsset(Asset entity) {
if (getAssetType().isInstance(entity)) {
//noinspection unchecked
return saveAndFlush((T) entity);
} else {
throw new ClassCastException("Entity is not of type " + getAssetType().getName());
}
}
Class<T> getAssetType();
long count();

View File

@@ -2,14 +2,19 @@ package be.seeseepuff.pcinv.services;
import be.seeseepuff.pcinv.meta.AssetDescriptor;
import be.seeseepuff.pcinv.meta.AssetDescriptors;
import be.seeseepuff.pcinv.meta.AssetInfo;
import be.seeseepuff.pcinv.meta.AssetProperty;
import be.seeseepuff.pcinv.models.Asset;
import be.seeseepuff.pcinv.models.GenericAsset;
import be.seeseepuff.pcinv.repositories.AssetRepository;
import be.seeseepuff.pcinv.repositories.GenericAssetRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Service for managing assets in the repository.
@@ -18,7 +23,7 @@ import java.util.List;
@Service
@RequiredArgsConstructor
public class AssetService {
private final GenericAssetRepository assetRepository;
private final GenericAssetRepository genericRepository;
private final Collection<AssetRepository<?>> repositories;
/**
@@ -27,7 +32,7 @@ public class AssetService {
* @return the total number of assets
*/
public long countAssets() {
return assetRepository.count();
return genericRepository.count();
}
/**
@@ -69,4 +74,103 @@ public class AssetService {
}
return List.of(getAssetDescriptor("asset"), getAssetDescriptor(type));
}
/**
* Creates a new asset of the specified type with the provided form data.
*
* @param type The type of asset to create.
* @param formData The form data containing the asset properties.
* @return The created asset.
*/
@Transactional
public Asset createAsset(String type, Map<String, String> formData) {
var genericDescriptor = getAssetDescriptor("asset");
var assetDescriptor = getAssetDescriptor(type);
var genericAsset = new GenericAsset();
fillIn(genericAsset, genericDescriptor, formData);
var asset = assetDescriptor.newInstance();
fillIn(asset, assetDescriptor, formData);
genericAsset = genericRepository.saveAndFlush(genericAsset);
asset.setAsset(genericAsset);
asset = getRepositoryFor(type).saveAndFlushAsset(asset);
return asset;
}
/**
* Gets the asset repository for the specified type.
*
* @param type the type of asset to get the repository for
* @return the AssetRepository for the specified type
*/
private AssetRepository<?> getRepositoryFor(String type) {
return repositories.stream()
.filter(repo -> repo.getAssetType().getAnnotation(AssetInfo.class).type().equals(type))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No repository found for type: " + type));
}
/**
* Fills in the properties of a generic asset from the form data.
*
* @param asset The generic asset to fill in.
* @param assetDescriptor The descriptor containing the properties to fill in.
* @param formData The form data containing the property values.
*/
private void fillIn(Object asset, AssetDescriptor assetDescriptor, Map<String, String> formData) {
for (var property : assetDescriptor.getProperties()) {
var value = parseValue(assetDescriptor, property, formData);
if (value != null) {
property.setValue(asset, value);
} else if (property.isRequired()) {
throw new IllegalArgumentException("Property '" + property.getName() + "' is required but not provided.");
}
}
}
/**
* Parses the string value into the appropriate type based on the asset property.
*
* @param descriptor The asset descriptor containing the property.
* @param property The asset property to determine the type.
* @param values The map of values from the form data.
* @return The parsed value as an Object.
*/
private Object parseValue(AssetDescriptor descriptor, AssetProperty property, Map<String, String> values) {
if (property.getType() == AssetProperty.Type.CAPACITY) {
var value = values.get(descriptor.asString(property) + "-value");
var unit = values.get(descriptor.asString(property) + "-unit");
if (value == null || value.isBlank() || unit == null || unit.isBlank()) {
return null;
}
try {
return Long.parseLong(value) * Long.parseLong(unit);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid numeric value for property '" + property.getName() + "': " + value, e);
}
}
var stringValue = values.get(descriptor.asString(property));
if (stringValue == null || stringValue.isBlank()) {
return null;
}
if (property.getType() == AssetProperty.Type.INTEGER) {
return Integer.parseInt(stringValue);
} else if (property.getType() == AssetProperty.Type.STRING) {
return stringValue;
} else if (property.getType().isEnum) {
for (var option : property.getOptions()) {
if (option.getValue().equals(stringValue)) {
return option.getEnumConstant();
}
}
throw new IllegalArgumentException("Invalid value for enum property '" + property.getName() + "': " + stringValue);
} else {
throw new IllegalArgumentException("Unsupported property type: " + property.getType());
}
}
}