This commit is contained in:
@@ -5,15 +5,50 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
/**
|
||||
* Controller for handling web requests related to assets.
|
||||
* Provides endpoints for viewing and creating assets.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Controller
|
||||
public class WebController {
|
||||
/// The name of the model attribute that holds the global asset descriptors.
|
||||
private static final String DESCRIPTORS = "descriptors";
|
||||
/// The name of the model attribute that holds the asset descriptor for the current view.
|
||||
private static final String DESCRIPTOR = "descriptor";
|
||||
|
||||
private final AssetService assetService;
|
||||
|
||||
/**
|
||||
* Handles the root URL and returns the index page with asset descriptors and asset count.
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index(Model model) {
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
|
||||
model.addAttribute("asset_count", assetService.countAssets());
|
||||
return "index";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a view where the user can create the type of asset to create.
|
||||
*/
|
||||
@GetMapping("/create")
|
||||
public String create(Model model) {
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
|
||||
return "create_select";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a view where the user can create a specific type of asset.
|
||||
*
|
||||
* @param type The type of asset to create.
|
||||
*/
|
||||
@GetMapping("/create/{type}")
|
||||
public String createType(Model model, @PathVariable String type) {
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type));
|
||||
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type));
|
||||
return "create_asset";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package be.seeseepuff.pcinv.meta;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -9,8 +10,9 @@ import java.util.Objects;
|
||||
/**
|
||||
* Describes the properties of an asset
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class AssetProperties {
|
||||
public class AssetDescriptor {
|
||||
/// The type of property, e.g.: ram, asset, etc...
|
||||
private final String type;
|
||||
|
||||
@@ -30,10 +32,10 @@ public class AssetProperties {
|
||||
* @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) {
|
||||
public static AssetDescriptor loadFrom(Class<?> assetType) {
|
||||
var assetInfo = assetType.getAnnotation(AssetInfo.class);
|
||||
Objects.requireNonNull(assetInfo, "Asset class must be annotated with @AssetInfo");
|
||||
return AssetProperties.builder()
|
||||
return AssetDescriptor.builder()
|
||||
.type(assetInfo.type())
|
||||
.displayName(assetInfo.displayName())
|
||||
.visible(assetInfo.isVisible())
|
||||
@@ -66,4 +68,14 @@ public class AssetProperties {
|
||||
builder.append(indent).append('}');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the asset property in the format "type-propertyName".
|
||||
*
|
||||
* @param property The asset property to format.
|
||||
* @return A string representation of the asset property.
|
||||
*/
|
||||
public String asString(AssetProperty property) {
|
||||
return String.format("%s-%s", type, property.getName());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,9 @@ package be.seeseepuff.pcinv.meta;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Holds the descriptors for all possible asset types.
|
||||
@@ -11,7 +12,7 @@ import java.util.Collection;
|
||||
@Getter
|
||||
public class AssetDescriptors {
|
||||
/// A collection of all types of assets.
|
||||
private final Collection<AssetProperties> assets = new ArrayList<>();
|
||||
private final Collection<AssetDescriptor> assets = new TreeSet<>(Comparator.comparing(AssetDescriptor::getDisplayName));
|
||||
|
||||
/**
|
||||
* Loads the asset properties from a given asset type.
|
||||
@@ -19,7 +20,7 @@ public class AssetDescriptors {
|
||||
* @param assetType The type of the asset to load properties for.
|
||||
*/
|
||||
public void loadFrom(Class<?> assetType) {
|
||||
var property = AssetProperties.loadFrom(assetType);
|
||||
var property = AssetDescriptor.loadFrom(assetType);
|
||||
assets.add(property);
|
||||
}
|
||||
|
||||
@@ -27,7 +28,7 @@ public class AssetDescriptors {
|
||||
public String toString() {
|
||||
var builder = new StringBuilder();
|
||||
builder.append("AssetDescriptors [\n");
|
||||
assets.forEach(assetProperties -> builder.append(assetProperties.toString(" ")).append('\n'));
|
||||
assets.forEach(assetDescriptor -> builder.append(assetDescriptor.toString(" ")).append('\n'));
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
15
src/main/java/be/seeseepuff/pcinv/meta/AssetEnum.java
Normal file
15
src/main/java/be/seeseepuff/pcinv/meta/AssetEnum.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package be.seeseepuff.pcinv.meta;
|
||||
|
||||
/**
|
||||
* An interface that should be implemented by all asset enums.
|
||||
*/
|
||||
public interface AssetEnum {
|
||||
/// Get the internal value of the enum.
|
||||
String getValue();
|
||||
|
||||
/// Get the display name of the enum.
|
||||
String getDisplayName();
|
||||
|
||||
/// Get the default value of the enum that should be selected when the user creates a device.
|
||||
boolean isDefaultValue();
|
||||
}
|
||||
18
src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java
Normal file
18
src/main/java/be/seeseepuff/pcinv/meta/AssetOption.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package be.seeseepuff.pcinv.meta;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Represents an option for an asset property enum.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class AssetOption {
|
||||
/// The internal value of the option.
|
||||
private final String value;
|
||||
/// The display name of the option.
|
||||
private final String displayName;
|
||||
/// Whether this option is the default value for the property.
|
||||
private final boolean isDefaultValue;
|
||||
}
|
||||
@@ -2,14 +2,19 @@ package be.seeseepuff.pcinv.meta;
|
||||
|
||||
import be.seeseepuff.pcinv.models.AssetCondition;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Singular;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class AssetProperty {
|
||||
/// The name of the property, e.g., "brand", "model", etc.
|
||||
@@ -18,12 +23,37 @@ public class AssetProperty {
|
||||
private final String displayName;
|
||||
/// The type of the property, which can be a string or an integer.
|
||||
private final Type type;
|
||||
/// Whether the property is required for the asset.
|
||||
private final boolean required;
|
||||
/// A set of options for the property, used for enum types.
|
||||
@Singular
|
||||
private final List<AssetOption> options;
|
||||
/// Whether the capacity should be displayed in SI units (e.g., GB, MB).
|
||||
private final boolean capacityAsSI;
|
||||
/// Whether the capacity should be displayed in IEC units (e.g., GiB, MiB).
|
||||
private final boolean capacityAsIEC;
|
||||
|
||||
/**
|
||||
* Enum representing the possible types of asset properties.
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public enum Type {
|
||||
STRING, INTEGER, CONDITION
|
||||
STRING(false),
|
||||
INTEGER(false),
|
||||
CAPACITY(false),
|
||||
CONDITION(true),
|
||||
;
|
||||
/// Set to `true` if the type is an enum, `false` otherwise.
|
||||
public final boolean isEnum;
|
||||
|
||||
/**
|
||||
* Returns the name of the type, or "enum" if it is an enum type.
|
||||
*
|
||||
* @return The name of the type or "enum" if it is an enum.
|
||||
*/
|
||||
public String nameOrEnum() {
|
||||
return isEnum ? "enum" : name();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,11 +68,34 @@ public class AssetProperty {
|
||||
if (annotation == null) {
|
||||
return null;
|
||||
}
|
||||
return AssetProperty.builder()
|
||||
|
||||
var type = determineType(property);
|
||||
var builder = AssetProperty.builder()
|
||||
.name(property.getName())
|
||||
.displayName(annotation.value())
|
||||
.type(determineType(property))
|
||||
.build();
|
||||
.type(type)
|
||||
.required(annotation.required());
|
||||
|
||||
if (type.isEnum) {
|
||||
var enumConstants = property.getType().getEnumConstants();
|
||||
for (var enumConstant : enumConstants) {
|
||||
if (!(enumConstant instanceof AssetEnum assetEnum)) {
|
||||
throw new IllegalArgumentException("Property " + enumConstant.getClass().getName() + " does not implement AssetEnum");
|
||||
}
|
||||
var option = AssetOption.builder()
|
||||
.value(assetEnum.getValue())
|
||||
.displayName(assetEnum.getDisplayName())
|
||||
.isDefaultValue(assetEnum.isDefaultValue())
|
||||
.build();
|
||||
builder.option(option);
|
||||
}
|
||||
}
|
||||
if (type == Type.CAPACITY) {
|
||||
var capacityAnnotation = property.getAnnotation(Capacity.class);
|
||||
builder.capacityAsSI(capacityAnnotation.si());
|
||||
builder.capacityAsIEC(capacityAnnotation.iec());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,6 +108,8 @@ public class AssetProperty {
|
||||
private static Type determineType(Field property) {
|
||||
if (property.getType() == String.class) {
|
||||
return Type.STRING;
|
||||
} else if (property.isAnnotationPresent(Capacity.class)) {
|
||||
return Type.CAPACITY;
|
||||
} 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) {
|
||||
@@ -66,6 +121,10 @@ public class AssetProperty {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s:%s (%s)", name, type.name(), displayName);
|
||||
var enumOptions = "";
|
||||
if (type.isEnum) {
|
||||
enumOptions = " [" + String.join(", ", options.stream().map(AssetOption::getValue).toList()) + "]";
|
||||
}
|
||||
return String.format("%s:%s (%s)%s", name, type.name(), displayName, enumOptions);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/main/java/be/seeseepuff/pcinv/meta/Capacity.java
Normal file
20
src/main/java/be/seeseepuff/pcinv/meta/Capacity.java
Normal 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 as representing some sort of capacity.
|
||||
* It is used to render the capacity in the UI as bytes, kilobytes, megabytes, etc.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface Capacity {
|
||||
/// Set to `true` if the capacity should include SI units (e.g., 1 KB = 1000 bytes).
|
||||
boolean si() default false;
|
||||
|
||||
/// Set to `true` if the capacity should be displayed in IEC units (e.g., 1 KiB = 1024 bytes).
|
||||
boolean iec() default true;
|
||||
}
|
||||
@@ -17,4 +17,11 @@ public @interface Property {
|
||||
* @return the display name of the property
|
||||
*/
|
||||
String value();
|
||||
|
||||
/**
|
||||
* Whether the property is required for the asset.
|
||||
*
|
||||
* @return true if the property is required, false otherwise
|
||||
*/
|
||||
boolean required() default false;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
package be.seeseepuff.pcinv.models;
|
||||
|
||||
import be.seeseepuff.pcinv.meta.AssetEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Represents the condition of an asset in the inventory system.
|
||||
*/
|
||||
public enum AssetCondition
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum AssetCondition implements AssetEnum
|
||||
{
|
||||
/// The asset is in perfect working order.
|
||||
HEALTHY,
|
||||
HEALTHY("healthy", "Healthy"),
|
||||
/// The condition of the asset is unknown. E.g.: it is untested.
|
||||
UNKNOWN,
|
||||
UNKNOWN("unknown", "Not known"),
|
||||
/// The asset generally works, but has some known issues.
|
||||
PARTIAL,
|
||||
PARTIAL("partial", "Partially working"),
|
||||
/// The asset is in need of repair, but is not completely broken.
|
||||
REPAIR,
|
||||
REPAIR("repair", "Requires repairs"),
|
||||
/// The asset is completely broken and cannot be used.
|
||||
BORKED,
|
||||
BORKED("borked", "Borked"),
|
||||
;
|
||||
private final String value;
|
||||
private final String displayName;
|
||||
|
||||
@Override
|
||||
public boolean isDefaultValue() {
|
||||
return this == UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class GenericAsset
|
||||
private long id;
|
||||
|
||||
/// The QR code attached to the asset, used for identification.
|
||||
@Property("QR")
|
||||
@Property(value = "QR", required = true)
|
||||
private long qr;
|
||||
|
||||
/// The brand of the asset.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package be.seeseepuff.pcinv.models;
|
||||
|
||||
import be.seeseepuff.pcinv.meta.AssetInfo;
|
||||
import be.seeseepuff.pcinv.meta.Capacity;
|
||||
import be.seeseepuff.pcinv.meta.Property;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
@@ -29,6 +30,7 @@ public class HddAsset implements Asset
|
||||
|
||||
/// The capacity of the drive in bytes.
|
||||
@Property("Capacity")
|
||||
@Capacity(si = true, iec = true)
|
||||
private long capacity;
|
||||
|
||||
/// The drive's interface type, such as SATA, IDE, ISA-16, ...
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package be.seeseepuff.pcinv.models;
|
||||
|
||||
import be.seeseepuff.pcinv.meta.AssetInfo;
|
||||
import be.seeseepuff.pcinv.meta.Capacity;
|
||||
import be.seeseepuff.pcinv.meta.Property;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
@@ -29,6 +30,7 @@ public class RamAsset implements Asset
|
||||
|
||||
/// The capacity of the RAM in bytes.
|
||||
@Property("Capacity")
|
||||
@Capacity
|
||||
private long capacity;
|
||||
|
||||
/// The type of memory. E.g.: DDR2, SDRAM, ISA-8, etc...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package be.seeseepuff.pcinv.services;
|
||||
|
||||
import be.seeseepuff.pcinv.meta.AssetDescriptor;
|
||||
import be.seeseepuff.pcinv.meta.AssetDescriptors;
|
||||
import be.seeseepuff.pcinv.models.GenericAsset;
|
||||
import be.seeseepuff.pcinv.repositories.AssetRepository;
|
||||
@@ -8,6 +9,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service for managing assets in the repository.
|
||||
@@ -28,6 +30,11 @@ public class AssetService {
|
||||
return assetRepository.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the global asset descriptors for all asset types.
|
||||
*
|
||||
* @return the AssetProperties for the specified type
|
||||
*/
|
||||
public AssetDescriptors getAssetDescriptors() {
|
||||
var descriptors = new AssetDescriptors();
|
||||
descriptors.loadFrom(GenericAsset.class);
|
||||
@@ -36,4 +43,30 @@ public class AssetService {
|
||||
}
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the asset properties for a specific type.
|
||||
*
|
||||
* @param type the type of asset to retrieve properties for
|
||||
* @return the AssetProperties for the specified type
|
||||
*/
|
||||
public AssetDescriptor getAssetDescriptor(String type) {
|
||||
return getAssetDescriptors().getAssets().stream()
|
||||
.filter(asset -> asset.getType().equals(type))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown asset type: " + type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a tree of asset descriptors for the specified type.
|
||||
*
|
||||
* @param type the type of asset to retrieve descriptors for
|
||||
* @return a list of AssetDescriptors for the specified type
|
||||
*/
|
||||
public List<AssetDescriptor> getAssetDescriptorTree(String type) {
|
||||
if (type.equals("asset")) {
|
||||
return List.of(getAssetDescriptor("asset"));
|
||||
}
|
||||
return List.of(getAssetDescriptor("asset"), getAssetDescriptor(type));
|
||||
}
|
||||
}
|
||||
|
||||
40
src/main/resources/templates/create_asset.html
Normal file
40
src/main/resources/templates/create_asset.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<body th:replace="fragments :: base(title='Select type to create', content=~{::content})">
|
||||
<div th:fragment="content">
|
||||
Create a <span th:text="${descriptor.displayName}"></span>
|
||||
<form th:action="'/asset/'+${descriptor.getType()}" method="post">
|
||||
<div th:each="d : ${descriptors}">
|
||||
<h2 th:text="${d.displayName}"></h2>
|
||||
<table border="1">
|
||||
<tr th:each="p : ${d.getProperties()}">
|
||||
<td><label th:text="${p.displayName}" th:for="${d.asString(p)}"></label></td>
|
||||
<td th:switch="${p.type.nameOrEnum()}">
|
||||
<input th:case="STRING" type="text" th:id="${d.asString(p)}" th:name="${d.asString(p)}" th:placeholder="${p.displayName}" th:required="${p.required}"/>
|
||||
<input th:case="INTEGER" type="number" th:id="${d.asString(p)}" th:name="${d.asString(p)}" th:required="${p.required}"/>
|
||||
<select th:case="enum" th:id="${d.asString(p)}" th:name="${d.asString(p)}">
|
||||
<option th:each="o : ${p.options}" th:value="${o.value}" th:text="${o.displayName}" th:selected="${o.defaultValue}">Good</option>
|
||||
</select>
|
||||
<span th:case="CAPACITY">
|
||||
<input type="number" th:id="${d.asString(p)+'-value'}" th:name="${d.asString(p)+'-value'}" th:required="${p.required}"/>
|
||||
<select th:id="${d.asString(p)}+'-unit'" th:name="${d.asString(p)}+'-unit'">
|
||||
<option value="1">Bytes</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000">kB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1024">KiB</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000000">MB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1048576">MiB</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000000000">GB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1073741824">GiB</option>
|
||||
<option th:if="${p.capacityAsSI}" value="1000000000000">TB</option>
|
||||
<option th:if="${p.capacityAsIEC}" value="1099511627776">TiB</option>
|
||||
</select>
|
||||
</span>
|
||||
<b th:case="*">Bad input type for <span th:text="${d.type}+'-'+${p.type}"></span></b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
<input type="submit" value="Create">
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
8
src/main/resources/templates/create_select.html
Normal file
8
src/main/resources/templates/create_select.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<body th:replace="fragments :: base(title='Select type to create', content=~{::content})">
|
||||
<div th:fragment="content">
|
||||
Create a new device
|
||||
<ul>
|
||||
<li th:each="d : ${descriptors.getAssets()}" th:if="${d.visible}"><a th:href="'/create/'+${d.getType()}" th:text="${d.displayName}"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:fragment="base(title, content)">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div th:replace="fragments :: base(title='Home', content=~{::content})">
|
||||
<div th:fragment="content">
|
||||
<a href="/create">Create a new device</a>
|
||||
<p>This system holds <span th:text="${asset_count}">5</span> assets.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user