Add ability to reflectively determine asset information
Some checks failed
Build / build (push) Failing after 1m41s

This commit is contained in:
2025-06-05 21:17:17 +02:00
parent 20bd00f67b
commit ca47f8f8ab
16 changed files with 416 additions and 52 deletions

View File

@@ -0,0 +1,23 @@
package be.seeseepuff.pcinv.controllers;
import be.seeseepuff.pcinv.services.AssetService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@RequiredArgsConstructor
@Service
public class StartupController {
private final AssetService assetService;
@PostConstruct
public void init() {
var descriptors = assetService.getAssetDescriptors();
log.info("Asset descriptors loaded:\n{}", descriptors);
var assets = assetService.countAssets();
log.info("The repository contains {} assets.", assets);
}
}

View File

@@ -0,0 +1,34 @@
package be.seeseepuff.pcinv.meta;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Collection;
/**
* Holds the descriptors for all possible asset types.
*/
@Getter
public class AssetDescriptors {
/// A collection of all types of assets.
private final Collection<AssetProperties> assets = new ArrayList<>();
/**
* Loads the asset properties from a given asset type.
*
* @param assetType The type of the asset to load properties for.
*/
public void loadFrom(Class<?> assetType) {
var property = AssetProperties.loadFrom(assetType);
assets.add(property);
}
@Override
public String toString() {
var builder = new StringBuilder();
builder.append("AssetDescriptors [\n");
assets.forEach(assetProperties -> builder.append(assetProperties.toString(" ")).append('\n'));
builder.append("]");
return builder.toString();
}
}

View File

@@ -0,0 +1,34 @@
package be.seeseepuff.pcinv.meta;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Provides metadata about an asset type itself.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AssetInfo {
/**
* The displayable name of the asset type.
*
* @return the display name of the asset type
*/
String displayName() default "";
/**
* The type of the asset, which can be a string or an integer.
*
* @return the type of the asset
*/
String type() default "";
/**
* Indicates whether the asset type should be visible in the UI.
*
* @return true if the asset type is visible, false otherwise
*/
boolean isVisible() default true;
}

View File

@@ -0,0 +1,69 @@
package be.seeseepuff.pcinv.meta;
import lombok.Builder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
/**
* Describes the properties of an asset
*/
@Builder
public class AssetProperties {
/// The type of property, e.g.: ram, asset, etc...
private final String type;
/// The displayable name of the property, e.g.: "Random Access memory"
private final String displayName;
/// Whether the asset is visible in the user interface.
private final boolean visible;
/// The properties of the asset, such as "brand", "model", etc.
@lombok.Singular
private Collection<AssetProperty> properties;
/**
* Loads the asset properties from a given asset class.
*
* @param assetType The class of the asset to load properties for.
* @return An AssetProperties instance containing the loaded properties.
*/
public static AssetProperties loadFrom(Class<?> assetType) {
var assetInfo = assetType.getAnnotation(AssetInfo.class);
Objects.requireNonNull(assetInfo, "Asset class must be annotated with @AssetInfo");
return AssetProperties.builder()
.type(assetInfo.type())
.displayName(assetInfo.displayName())
.visible(assetInfo.isVisible())
.properties(Arrays.stream(assetType.getDeclaredFields())
.map(AssetProperty::loadFrom)
.filter(Objects::nonNull)
.toList())
.build();
}
/**
* Returns a string representation of the asset properties with the specified indentation.
*
* @param indent The indentation to use for formatting.
* @return A formatted string representation of the asset properties.
*/
public String toString(String indent) {
var builder = new StringBuilder();
builder.append(indent).append("AssetProperties {\n");
builder.append(indent).append(" type='").append(type).append("',\n");
builder.append(indent).append(" displayName='").append(displayName).append("',\n");
if (!visible) {
builder.append(indent).append(" hidden").append(",\n");
}
builder.append(indent).append(" properties=[\n");
for (var property : properties) {
builder.append(indent).append(" ").append(property.toString()).append("\n");
}
builder.append(indent).append(" ]\n");
builder.append(indent).append('}');
return builder.toString();
}
}

View File

@@ -0,0 +1,71 @@
package be.seeseepuff.pcinv.meta;
import be.seeseepuff.pcinv.models.AssetCondition;
import jakarta.annotation.Nullable;
import lombok.Builder;
import java.lang.reflect.Field;
/**
* Represents a property of an asset, such as its name or type.
* This class is used to define the metadata for assets in the system.
*/
@Builder
public class AssetProperty {
/// The name of the property, e.g., "brand", "model", etc.
private final String name;
/// The name of the property as it should be displayed, e.g., "Brand", "Model", etc.
private final String displayName;
/// The type of the property, which can be a string or an integer.
private final Type type;
/**
* Enum representing the possible types of asset properties.
*/
public enum Type {
STRING, INTEGER, CONDITION
}
/**
* Loads an AssetProperty from a given field.
*
* @param property The field representing the property.
* @return An AssetProperty instance with the name, display name, and type determined from the field.
*/
@Nullable
public static AssetProperty loadFrom(Field property) {
var annotation = property.getAnnotation(Property.class);
if (annotation == null) {
return null;
}
return AssetProperty.builder()
.name(property.getName())
.displayName(annotation.value())
.type(determineType(property))
.build();
}
/**
* Determines the type of the property based on its field type.
*
* @param property The field representing the property.
* @return The type of the property.
* @throws IllegalArgumentException if the property type is unsupported.
*/
private static Type determineType(Field property) {
if (property.getType() == String.class) {
return Type.STRING;
} else if (property.getType() == Integer.class || property.getType() == int.class || property.getType() == Long.class || property.getType() == long.class) {
return Type.INTEGER;
} else if (property.getType() == AssetCondition.class) {
return Type.CONDITION;
} else {
throw new IllegalArgumentException("Unsupported property type: " + property.getType());
}
}
@Override
public String toString() {
return String.format("%s:%s (%s)", name, type.name(), displayName);
}
}

View File

@@ -0,0 +1,20 @@
package be.seeseepuff.pcinv.meta;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation to mark a property descriptor of an asset.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Property {
/**
* The displayable name of the property.
*
* @return the display name of the property
*/
String value();
}

View File

@@ -1,37 +1,7 @@
package be.seeseepuff.pcinv.models;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
public interface Asset {
long getId();
/**
* Represents a generic asset in the inventory system.
*/
@Getter
@Setter
@Entity
public class Asset
{
@Id @GeneratedValue
private Long id;
/// The QR code attached to the asset, used for identification.
private Long qr;
/// The brand of the asset.
private String brand;
/// The model of the asset
private String model;
/// The asset's serial number.
private String serialNumber;
/// A description of the asset, providing additional details.
private String description;
/// The state of the asset, indicating its condition.
private AssetCondition condition;
GenericAsset getAsset();
}

View File

@@ -0,0 +1,52 @@
package be.seeseepuff.pcinv.models;
import be.seeseepuff.pcinv.meta.AssetInfo;
import be.seeseepuff.pcinv.meta.Property;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* Represents a generic asset in the inventory system.
*/
@Getter
@Setter
@Entity
@AssetInfo(
displayName = "Asset",
type = "asset",
isVisible = false
)
@Table(name = "assets")
public class GenericAsset
{
@Id @GeneratedValue
private long id;
/// The QR code attached to the asset, used for identification.
@Property("QR")
private long qr;
/// The brand of the asset.
@Property("Brand")
private String brand;
/// The model of the asset
@Property("Model")
private String model;
/// The asset's serial number.
@Property("Serial Number")
private String serialNumber;
/// A description of the asset, providing additional details.
@Property("Description")
private String description;
/// The state of the asset, indicating its condition.
@Property("Condition")
private AssetCondition condition;
}

View File

@@ -1,8 +1,8 @@
package be.seeseepuff.pcinv.models;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import be.seeseepuff.pcinv.meta.AssetInfo;
import be.seeseepuff.pcinv.meta.Property;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@@ -12,21 +12,30 @@ import lombok.Setter;
@Getter
@Setter
@Entity
public class HddAsset
@AssetInfo(
displayName = "Hard Drive",
type = "HDD"
)
@Table(name = "hdd_assets")
public class HddAsset implements Asset
{
@Id
@GeneratedValue
private Long id;
private long id;
/// The ID of the associated asset, linking it to the generic Asset model.
private Long assetId;
/// The generic asset associated with this HDD.
@OneToOne(orphanRemoval = true)
private GenericAsset asset;
/// The capacity of the drive in bytes.
private Long capacity;
@Property("Capacity")
private long capacity;
/// The drive's interface type, such as SATA, IDE, ISA-16, ...
@Property("Interface Type")
private String interfaceType;
/// The drive's form factor, such as 2.5", 3.5", etc.
@Property("Form Factor")
private String formFactor;
}

View File

@@ -1,8 +1,8 @@
package be.seeseepuff.pcinv.models;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import be.seeseepuff.pcinv.meta.AssetInfo;
import be.seeseepuff.pcinv.meta.Property;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@@ -12,18 +12,26 @@ import lombok.Setter;
@Getter
@Setter
@Entity
public class RamAsset
@AssetInfo(
displayName = "Random Access Memory",
type = "RAM"
)
@Table(name = "ram_assets")
public class RamAsset implements Asset
{
@Id
@GeneratedValue
private Long id;
private long id;
/// The ID of the associated asset, linking it to the generic Asset model.
private Long assetId;
/// The generic asset associated with this RAM.
@OneToOne(orphanRemoval = true)
private GenericAsset asset;
/// The capacity of the RAM in bytes.
private Long capacity;
@Property("Capacity")
private long capacity;
/// The type of memory. E.g.: DDR2, SDRAM, ISA-8, etc...
@Property("Type")
private String type;
}

View File

@@ -1,8 +1,11 @@
package be.seeseepuff.pcinv.repositories;
import be.seeseepuff.pcinv.models.Asset;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AssetRepository extends CrudRepository<Asset, Long> {}
public interface AssetRepository<T extends Asset> {
Class<T> getAssetType();
long count();
}

View File

@@ -0,0 +1,8 @@
package be.seeseepuff.pcinv.repositories;
import be.seeseepuff.pcinv.models.GenericAsset;
import org.springframework.data.jpa.repository.JpaRepository;
@SuppressWarnings("unused")
public interface GenericAssetRepository extends JpaRepository<GenericAsset, Long> {
}

View File

@@ -0,0 +1,12 @@
package be.seeseepuff.pcinv.repositories;
import be.seeseepuff.pcinv.models.HddAsset;
import org.springframework.data.jpa.repository.JpaRepository;
@SuppressWarnings("unused")
public interface HddAssetRepository extends JpaRepository<HddAsset, Long>, AssetRepository<HddAsset> {
@Override
default Class<HddAsset> getAssetType() {
return HddAsset.class;
}
}

View File

@@ -0,0 +1,12 @@
package be.seeseepuff.pcinv.repositories;
import be.seeseepuff.pcinv.models.RamAsset;
import org.springframework.data.jpa.repository.JpaRepository;
@SuppressWarnings("unused")
public interface RamAssetRepository extends JpaRepository<RamAsset, Long>, AssetRepository<RamAsset> {
@Override
default Class<RamAsset> getAssetType() {
return RamAsset.class;
}
}

View File

@@ -0,0 +1,39 @@
package be.seeseepuff.pcinv.services;
import be.seeseepuff.pcinv.meta.AssetDescriptors;
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 java.util.Collection;
/**
* Service for managing assets in the repository.
* Provides methods to interact with the asset repositories.
*/
@Service
@RequiredArgsConstructor
public class AssetService {
private final GenericAssetRepository assetRepository;
private final Collection<AssetRepository<?>> repositories;
/**
* Returns the count of all assets in the repository.
*
* @return the total number of assets
*/
public long countAssets() {
return assetRepository.count();
}
public AssetDescriptors getAssetDescriptors() {
var descriptors = new AssetDescriptors();
descriptors.loadFrom(GenericAsset.class);
for (var repository : repositories) {
descriptors.loadFrom(repository.getAssetType());
}
return descriptors;
}
}

View File

@@ -3,4 +3,4 @@ server.port=8088
spring.datasource.url=jdbc:postgresql://localhost:5432/pcinv
spring.datasource.username=pcinv
spring.datasource.password=pcinv
a
spring.jpa.hibernate.ddl-auto=update