From 257abddc1591d8fe38d4f3b31c259be6b2a2bb01 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Sun, 8 Jun 2025 06:10:45 +0200 Subject: [PATCH] Actually support creating assets --- .../pcinv/controllers/WebController.java | 13 ++- .../pcinv/meta/AssetDescriptor.java | 34 +++++- .../be/seeseepuff/pcinv/meta/AssetOption.java | 2 + .../seeseepuff/pcinv/meta/AssetProperty.java | 27 ++++- .../be/seeseepuff/pcinv/models/Asset.java | 4 + .../be/seeseepuff/pcinv/models/HddAsset.java | 2 +- .../be/seeseepuff/pcinv/models/RamAsset.java | 2 +- .../pcinv/repositories/AssetRepository.java | 11 ++ .../pcinv/services/AssetService.java | 108 +++++++++++++++++- 9 files changed, 192 insertions(+), 11 deletions(-) diff --git a/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java b/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java index 5b303c2..14842ea 100644 --- a/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java +++ b/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java @@ -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 formData) { - model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type)); - model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type)); - return "create_asset"; + var formMap = new HashMap(); + formData.forEach((key, values) -> { + if (!values.isEmpty()) { + formMap.put(key, values.getFirst()); + } + }); + var asset = assetService.createAsset(type, formMap); + return "redirect:/view/" + asset.getQr(); } } diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java index 5acb1a8..517be6a 100644 --- a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java @@ -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 properties; + /// A supplier that can be used to create a new instance of the asset type described by this descriptor. + private Supplier 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(); + } } diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java index 1322fa1..aa22bf2 100644 --- a/src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java @@ -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; } diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java index 7578104..78fe0e6 100644 --- a/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java @@ -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 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 = ""; diff --git a/src/main/java/be/seeseepuff/pcinv/models/Asset.java b/src/main/java/be/seeseepuff/pcinv/models/Asset.java index aec1485..0d97e25 100644 --- a/src/main/java/be/seeseepuff/pcinv/models/Asset.java +++ b/src/main/java/be/seeseepuff/pcinv/models/Asset.java @@ -5,5 +5,9 @@ public interface Asset { GenericAsset getAsset(); + default long getQr() { + return getAsset().getQr(); + } + void setAsset(GenericAsset asset); } diff --git a/src/main/java/be/seeseepuff/pcinv/models/HddAsset.java b/src/main/java/be/seeseepuff/pcinv/models/HddAsset.java index b81da1a..305a07f 100644 --- a/src/main/java/be/seeseepuff/pcinv/models/HddAsset.java +++ b/src/main/java/be/seeseepuff/pcinv/models/HddAsset.java @@ -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") diff --git a/src/main/java/be/seeseepuff/pcinv/models/RamAsset.java b/src/main/java/be/seeseepuff/pcinv/models/RamAsset.java index 0150ca8..787306f 100644 --- a/src/main/java/be/seeseepuff/pcinv/models/RamAsset.java +++ b/src/main/java/be/seeseepuff/pcinv/models/RamAsset.java @@ -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") diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java index 09aa689..36b837c 100644 --- a/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java +++ b/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java @@ -5,6 +5,17 @@ import org.springframework.stereotype.Repository; @Repository public interface AssetRepository { + 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 getAssetType(); long count(); diff --git a/src/main/java/be/seeseepuff/pcinv/services/AssetService.java b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java index 5596de7..8d494db 100644 --- a/src/main/java/be/seeseepuff/pcinv/services/AssetService.java +++ b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java @@ -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> 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 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 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 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()); + } + } }