From a9825b2074e60bcdc9250f842a01d4837bf16a27 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 2 May 2024 17:57:28 +0200 Subject: [PATCH 1/7] hosting asset validation for Cloud-Server to Webspace (WIP) --- .../asset/HsHostingAssetController.java | 7 +- .../HsHostingAssetPropertyValidator.java | 95 +++++++++++++++++++ .../asset/HsHostingAssetValidator.java | 51 ++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 62a62b34..b3411576 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -60,7 +60,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = assetRepo.save(entityToSave); + final var saved = assetRepo.save(valid(entityToSave)); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -71,6 +71,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi { return ResponseEntity.created(uri).body(mapped); } + private HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) { + HsHostingAssetValidator.forType(entityToSave.getType()).validate(entityToSave); + return entityToSave; + } + @Override @Transactional(readOnly = true) public ResponseEntity getAssetByUuid( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java new file mode 100644 index 00000000..7dd4154d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java @@ -0,0 +1,95 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import lombok.With; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +class Validator { +} + +@SuperBuilder +class PropertyValidator extends Validator { + String propertyName; +} + +@SuperBuilder +class NumericPropertyValidator extends PropertyValidator { + + private String unit; + private Integer min; + private Integer max; + private Integer step; + + static NumericPropertyValidatorBuilder numericProperty(final String propertyName) { + return NumericPropertyValidator.builder().propertyName(propertyName); + } +} + +@With +@SuperBuilder +class EnumPropertyValidator extends PropertyValidator { + + private String[] values; + + static EnumPropertyValidatorBuilderExtension enumProperty(final String propertyName) { + return new EnumPropertyValidatorBuilderExtension(propertyName); + } + static class EnumPropertyValidatorBuilderExtension extends ValidatorBuilder { + + private final String propertyName; + private String[] values; + + EnumPropertyValidatorBuilderExtension(final String propertyName) { + this.propertyName = propertyName; + } + + + public EnumPropertyValidatorBuilderExtension values(final String... values) { + this.values = values; + return this; + } + + @Override + protected EnumPropertyValidatorBuilderExtension self() { + return this; + } + + @Override + public EnumPropertyValidator build() { + return EnumPropertyValidator.builder().propertyName(propertyName).values(values).build(); + } + } +} + +@SuperBuilder +class BooleanPropertyValidator extends PropertyValidator { + + private Map.Entry falseIf; + static BooleanPropertyValidatorBuilderExtension booleanProperty(final String propertyName) { + return new BooleanPropertyValidatorBuilderExtension(propertyName); + } + + static class BooleanPropertyValidatorBuilderExtension extends PropertyValidatorBuilder { + + + BooleanPropertyValidatorBuilderExtension(final String propertyName) { + super.propertyName(propertyName); + } + + + BooleanPropertyValidatorBuilderExtension falseIf(final String propertyName, final String propertyValue) { + return this; + } + + @Override + protected BooleanPropertyValidatorBuilderExtension self() { + return this; + } + + @Override + public BooleanPropertyValidator build() { + return new BooleanPropertyValidator() + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java new file mode 100644 index 00000000..95f94124 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java @@ -0,0 +1,51 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.BooleanPropertyValidator.booleanProperty; +import static net.hostsharing.hsadminng.hs.hosting.asset.EnumPropertyValidator.enumProperty; +import static net.hostsharing.hsadminng.hs.hosting.asset.NumericPropertyValidator.numericProperty; + +public class HsHostingAssetValidator { + + private static final Map validators = Map.ofEntries( + entry(HsHostingAssetType.CLOUD_SERVER, new HsHostingAssetValidator( + numericProperty("CPUs").min(1).max(32).build(), + numericProperty("RAM").unit("GB").min(1).max(128).build(), + numericProperty("SSD").unit("GB").min(25).max(1000).step(25).build(), + numericProperty("HDD").unit("GB").min(0).max(4000).step(250).build(), + numericProperty("Traffic").unit("GB").min(250).max(10000).step(250).build(), + enumProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").build())), + entry(HsHostingAssetType.MANAGED_SERVER, new HsHostingAssetValidator( + numericProperty("CPUs").min(1).max(32).build(), + numericProperty("RAM").unit("GB").min(1).max(128).build(), + numericProperty("SSD").unit("GB").min(25).max(1000).step(25).build(), + numericProperty("HDD").unit("GB").min(0).max(4000).step(250).build(), + numericProperty("Traffic").unit("GB").min(250).max(10000).step(250).build(), + enumProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").build(), + booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").build(), + booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").build(), + booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").build(), + booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").build(), + booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").build())), + entry(HsHostingAssetType.MANAGED_WEBSPACE, new HsHostingAssetValidator( + numericProperty("SSD").unit("GB").min(1).max(100).step(1).build(), + numericProperty("HDD").unit("GB").min(0).max(250).step(10).build(), + numericProperty("Traffic").unit("GB").min(10).max(1000).step(10).build(), + enumProperty("SLA-Platform").values("BASIC", "EXT24H").build(), + numericProperty("Daemons").min(0).max(10).build(), + booleanProperty("Online Office Server").build()) + )); + + public static HsHostingAssetValidator forType(final HsHostingAssetType type) { + return validators.get(type); + } + + HsHostingAssetValidator(final Validator... validators) { + + } + + public void validate(final HsHostingAssetEntity entityToSave) { + } +} -- 2.39.5 From faaca44bea60bd6c75077fcd2c17f13a2b9eb7f0 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 3 May 2024 14:50:05 +0200 Subject: [PATCH 2/7] hosting asset validation for Cloud-Server to Webspace --- .../asset/HsHostingAssetController.java | 15 +- .../HsHostingAssetPropertyValidator.java | 95 ----------- .../asset/HsHostingAssetValidator.java | 51 ------ .../HsHostingAssetPropertyValidator.java | 148 ++++++++++++++++++ .../validator/HsHostingAssetValidator.java | 75 +++++++++ .../hs/hosting/asset/validator/lombok.config | 3 + .../hsadminng/arch/ArchitectureTest.java | 1 + ...sHostingAssetControllerAcceptanceTest.java | 37 ++++- .../HsHostingAssetValidatorUnitTest.java | 97 ++++++++++++ 9 files changed, 369 insertions(+), 153 deletions(-) delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index b3411576..384fc2e3 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.context.Context; @@ -15,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.validation.ValidationException; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; @@ -71,11 +73,6 @@ public class HsHostingAssetController implements HsHostingAssetsApi { return ResponseEntity.created(uri).body(mapped); } - private HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) { - HsHostingAssetValidator.forType(entityToSave.getType()).validate(entityToSave); - return entityToSave; - } - @Override @Transactional(readOnly = true) public ResponseEntity getAssetByUuid( @@ -125,6 +122,14 @@ public class HsHostingAssetController implements HsHostingAssetsApi { return ResponseEntity.ok(mapped); } + private HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) { + final var violations = HsHostingAssetValidator.forType(entityToSave.getType()).validate(entityToSave); + if (!violations.isEmpty()) { + throw new ValidationException(violations.toString()); + } + return entityToSave; + } + @SuppressWarnings("unchecked") final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.putConfig(KeyValueMap.from(resource.getConfig())); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java deleted file mode 100644 index 7dd4154d..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropertyValidator.java +++ /dev/null @@ -1,95 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset; - -import lombok.With; -import lombok.experimental.SuperBuilder; - -import java.util.Map; - -class Validator { -} - -@SuperBuilder -class PropertyValidator extends Validator { - String propertyName; -} - -@SuperBuilder -class NumericPropertyValidator extends PropertyValidator { - - private String unit; - private Integer min; - private Integer max; - private Integer step; - - static NumericPropertyValidatorBuilder numericProperty(final String propertyName) { - return NumericPropertyValidator.builder().propertyName(propertyName); - } -} - -@With -@SuperBuilder -class EnumPropertyValidator extends PropertyValidator { - - private String[] values; - - static EnumPropertyValidatorBuilderExtension enumProperty(final String propertyName) { - return new EnumPropertyValidatorBuilderExtension(propertyName); - } - static class EnumPropertyValidatorBuilderExtension extends ValidatorBuilder { - - private final String propertyName; - private String[] values; - - EnumPropertyValidatorBuilderExtension(final String propertyName) { - this.propertyName = propertyName; - } - - - public EnumPropertyValidatorBuilderExtension values(final String... values) { - this.values = values; - return this; - } - - @Override - protected EnumPropertyValidatorBuilderExtension self() { - return this; - } - - @Override - public EnumPropertyValidator build() { - return EnumPropertyValidator.builder().propertyName(propertyName).values(values).build(); - } - } -} - -@SuperBuilder -class BooleanPropertyValidator extends PropertyValidator { - - private Map.Entry falseIf; - static BooleanPropertyValidatorBuilderExtension booleanProperty(final String propertyName) { - return new BooleanPropertyValidatorBuilderExtension(propertyName); - } - - static class BooleanPropertyValidatorBuilderExtension extends PropertyValidatorBuilder { - - - BooleanPropertyValidatorBuilderExtension(final String propertyName) { - super.propertyName(propertyName); - } - - - BooleanPropertyValidatorBuilderExtension falseIf(final String propertyName, final String propertyValue) { - return this; - } - - @Override - protected BooleanPropertyValidatorBuilderExtension self() { - return this; - } - - @Override - public BooleanPropertyValidator build() { - return new BooleanPropertyValidator() - } - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java deleted file mode 100644 index 95f94124..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidator.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset; - -import java.util.Map; - -import static java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.hosting.asset.BooleanPropertyValidator.booleanProperty; -import static net.hostsharing.hsadminng.hs.hosting.asset.EnumPropertyValidator.enumProperty; -import static net.hostsharing.hsadminng.hs.hosting.asset.NumericPropertyValidator.numericProperty; - -public class HsHostingAssetValidator { - - private static final Map validators = Map.ofEntries( - entry(HsHostingAssetType.CLOUD_SERVER, new HsHostingAssetValidator( - numericProperty("CPUs").min(1).max(32).build(), - numericProperty("RAM").unit("GB").min(1).max(128).build(), - numericProperty("SSD").unit("GB").min(25).max(1000).step(25).build(), - numericProperty("HDD").unit("GB").min(0).max(4000).step(250).build(), - numericProperty("Traffic").unit("GB").min(250).max(10000).step(250).build(), - enumProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").build())), - entry(HsHostingAssetType.MANAGED_SERVER, new HsHostingAssetValidator( - numericProperty("CPUs").min(1).max(32).build(), - numericProperty("RAM").unit("GB").min(1).max(128).build(), - numericProperty("SSD").unit("GB").min(25).max(1000).step(25).build(), - numericProperty("HDD").unit("GB").min(0).max(4000).step(250).build(), - numericProperty("Traffic").unit("GB").min(250).max(10000).step(250).build(), - enumProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").build(), - booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").build(), - booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").build(), - booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").build(), - booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").build(), - booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").build())), - entry(HsHostingAssetType.MANAGED_WEBSPACE, new HsHostingAssetValidator( - numericProperty("SSD").unit("GB").min(1).max(100).step(1).build(), - numericProperty("HDD").unit("GB").min(0).max(250).step(10).build(), - numericProperty("Traffic").unit("GB").min(10).max(1000).step(10).build(), - enumProperty("SLA-Platform").values("BASIC", "EXT24H").build(), - numericProperty("Daemons").min(0).max(10).build(), - booleanProperty("Online Office Server").build()) - )); - - public static HsHostingAssetValidator forType(final HsHostingAssetType type) { - return validators.get(type); - } - - HsHostingAssetValidator(final Validator... validators) { - - } - - public void validate(final HsHostingAssetEntity entityToSave) { - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java new file mode 100644 index 00000000..71a1ec13 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java @@ -0,0 +1,148 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validator; + +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@RequiredArgsConstructor +public abstract class HsHostingAssetPropertyValidator { + + final Class type; + final String propertyName; + private Boolean required; + + public static Map.Entry defType(K k, V v) { + return new SimpleImmutableEntry<>(k, v); + } + + public HsHostingAssetPropertyValidator required() { + required = Boolean.TRUE; + return this; + } + + public HsHostingAssetPropertyValidator optional() { + required = Boolean.FALSE; + return this; + } + + public final List validate(final Map props) { + final var result = new ArrayList(); + final var propValue = props.get(propertyName); + if (propValue == null) { + if (required) { + result.add("'" + propertyName + "' is required but missing"); + } + } + if (propValue != null){ + if ( type.isInstance(propValue)) { + //noinspection unchecked + validate(result, (T) propValue, props); + } else { + result.add("'" + propertyName + "' is expected to be of type " + type + ", " + + "but is of type '" + propValue.getClass().getSimpleName() + "'"); + } + } + return result; + } + + protected abstract void validate(final ArrayList result, final T propValue, final Map props); + + public void verifyConsistency(final Map.Entry typeDef) { + if (required == null ) { + throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); + } + } +} + +@Setter +class IntegerPropertyValidator extends HsHostingAssetPropertyValidator{ + + private String unit; + private Integer min; + private Integer max; + private Integer step; + + public static IntegerPropertyValidator integerProperty(final String propertyName) { + return new IntegerPropertyValidator(propertyName); + } + + private IntegerPropertyValidator(final String propertyName) { + super(Integer.class, propertyName); + } + + + @Override + protected void validate(final ArrayList result, final Integer propValue, final Map props) { + if (min != null && propValue < min) { + result.add("'" + propertyName + "' is expected to be >= " + min + " but is " + propValue); + } + if (max != null && propValue > max) { + result.add("'" + propertyName + "' is expected to be <= " + max + " but is " + propValue); + } + if (step != null && propValue % step != 0) { + result.add("'" + propertyName + "' is expected to be multiple of " + step + " but is " + propValue); + } + } +} + +@Setter +class EnumPropertyValidator extends HsHostingAssetPropertyValidator { + + private String[] values; + + private EnumPropertyValidator(final String propertyName) { + super(String.class, propertyName); + } + + public static EnumPropertyValidator enumProperty(final String propertyName) { + return new EnumPropertyValidator(propertyName); + } + + public HsHostingAssetPropertyValidator values(final String... values) { + this.values = values; + return this; + } + + @Override + protected void validate(final ArrayList result, final String propValue, final Map props) { + if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { + result.add("'" + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); + } + } +} + +@Setter +class BooleanPropertyValidator extends HsHostingAssetPropertyValidator { + + private Map.Entry falseIf; + + private BooleanPropertyValidator(final String propertyName) { + super(Boolean.class, propertyName); + } + + public static BooleanPropertyValidator booleanProperty(final String propertyName) { + return new BooleanPropertyValidator(propertyName); + } + + HsHostingAssetPropertyValidator falseIf(final String refPropertyName, final String refPropertyValue) { + this.falseIf = new SimpleImmutableEntry<>(refPropertyName, refPropertyValue); + return this; + } + + @Override + protected void validate(final ArrayList result, final Boolean propValue, final Map props) { + if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) { + if (propValue) { + result.add("'" + propertyName + "' is expected to be false because " + + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue); + } + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java new file mode 100644 index 00000000..26d4c5e4 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java @@ -0,0 +1,75 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validator; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.stream; +import static net.hostsharing.hsadminng.hs.hosting.asset.validator.EnumPropertyValidator.enumProperty; +import static net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetPropertyValidator.defType; +import static net.hostsharing.hsadminng.hs.hosting.asset.validator.BooleanPropertyValidator.booleanProperty; +import static net.hostsharing.hsadminng.hs.hosting.asset.validator.IntegerPropertyValidator.integerProperty; + +public class HsHostingAssetValidator { + + private static final Map validators = Map.ofEntries( + defType(HsHostingAssetType.CLOUD_SERVER, new HsHostingAssetValidator( + integerProperty("CPUs").min(1).max(32).required(), + integerProperty("RAM").unit("GB").min(1).max(128).required(), + integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), + integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), + integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), + enumProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional())), + defType(HsHostingAssetType.MANAGED_SERVER, new HsHostingAssetValidator( + integerProperty("CPUs").min(1).max(32).required(), + integerProperty("RAM").unit("GB").min(1).max(128).required(), + integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), + integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), + integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), + enumProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(), + booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(), + booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(), + booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(), + booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(), + booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").optional())), + defType(HsHostingAssetType.MANAGED_WEBSPACE, new HsHostingAssetValidator( + integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(), + integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(), + integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(), + enumProperty("SLA-Platform").values("BASIC", "EXT24H").optional(), + integerProperty("Daemons").min(0).max(10).optional(), + booleanProperty("Online Office Server").optional()) + )); + static { + validators.entrySet().forEach(typeDef -> { + stream(typeDef.getValue().propertyValidators).forEach( entry -> { + entry.verifyConsistency(typeDef); + }); + }); + } + private final HsHostingAssetPropertyValidator[] propertyValidators; + + public static HsHostingAssetValidator forType(final HsHostingAssetType type) { + return validators.get(type); + } + + HsHostingAssetValidator(final HsHostingAssetPropertyValidator... validators) { + propertyValidators = validators; + } + + public List validate(final HsHostingAssetEntity assetEntity) { + final var result = new ArrayList(); + assetEntity.getConfig().keySet().forEach( givenPropName -> { + if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) { + result.add("'" + givenPropName + "' is not expected but is '" +assetEntity.getConfig().get(givenPropName) + "'"); + } + }); + stream(propertyValidators).forEach(pv -> { + result.addAll(pv.validate(assetEntity.getConfig())); + }); + return result; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config new file mode 100644 index 00000000..18183936 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/lombok.config @@ -0,0 +1,3 @@ +lombok.addLombokGeneratedAnnotation = true +lombok.accessors.chain = true +lombok.accessors.fluent = true diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 15f9c152..0cb1a086 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -52,6 +52,7 @@ public class ArchitectureTest { "..hs.office.sepamandate", "..hs.booking.item", "..hs.hosting.asset", + "..hs.hosting.asset.validator", "..errors", "..mapper", "..ping", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 26d1b763..0cde4075 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -174,7 +174,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm1400", "caption": "some new CloudServer", - "config": { "CPU": 3, "extra": 42 } + "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } } """.formatted(givenBookingItem.getUuid())) .port(port) @@ -188,7 +188,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm1400", "caption": "some new CloudServer", - "config": { "CPU": 3, "extra": 42 } + "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } } """)) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) @@ -199,6 +199,39 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup location.substring(location.lastIndexOf('/') + 1)); assertThat(newUserUuid).isNotNull(); } + + @Test + void additionalValidationsArePerformend_whenAddingAsset() { + + context.define("superuser-alex@hostsharing.net"); + final var givenBookingItem = givenBookingItem("First", "some PrivateCloud"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "bookingItemUuid": "%s", + "type": "MANAGED_SERVER", + "identifier": "vm1400", + "caption": "some new CloudServer", + "config": { "CPUs": 0, "extra": 42 } + } + """.formatted(givenBookingItem.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/hosting/assets") + .then().log().all().assertThat() + .statusCode(400) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "statusPhrase": "Bad Request", + "message": "['extra' is not expected but is '42', 'CPUs' is expected to be >= 1 but is 0, 'RAM' is required but missing, 'SSD' is required but missing, 'Traffic' is required but missing]" + } + """)); // @formatter:on + } } @Nested diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..d7f21222 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetValidatorUnitTest.java @@ -0,0 +1,97 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Collections.emptyMap; +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; +import static org.assertj.core.api.Assertions.assertThat; + +class HsHostingAssetValidatorUnitTest { + + @Test + void validatesMissingProperties() { + // given + final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE); + final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_WEBSPACE) + .config(emptyMap()) + .build(); + + // when + final var result = validator.validate(mangedWebspaceHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'SSD' is required but missing", + "'Traffic' is required but missing" + ); + } + + @Test + void validatesUnknownProperties() { + // given + final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE); + final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_WEBSPACE) + .config(Map.ofEntries( + entry("HDD", 0), + entry("SSD", 1), + entry("Traffic", 10), + entry("unknown", "some value") + )) + .build(); + + // when + final var result = validator.validate(mangedWebspaceHostingAssetEntity); + + // then + assertThat(result).containsExactly("'unknown' is not expected but is 'some value'"); + } + + @Test + void validatesDependentProperties() { + // given + final var validator = HsHostingAssetValidator.forType(MANAGED_SERVER); + final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_SERVER) + .config(Map.ofEntries( + entry("CPUs", 2), + entry("RAM", 25), + entry("SSD", 25), + entry("Traffic", 250), + entry("SLA-EMail", true) + )) + .build(); + + // when + final var result = validator.validate(mangedWebspaceHostingAssetEntity); + + // then + assertThat(result).containsExactly("'SLA-EMail' is expected to be false because SLA-Platform=BASIC but is true"); + } + + @Test + void validatesValidProperties() { + // given + final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE); + final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_WEBSPACE) + .config(Map.ofEntries( + entry("HDD", 200), + entry("SSD", 25), + entry("Traffic", 250) + )) + .build(); + + // when + final var result = validator.validate(mangedWebspaceHostingAssetEntity); + + // then + assertThat(result).isEmpty(); + } +} -- 2.39.5 From 05f9e8142e200bd920e1a761e32a08704dfbfd64 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 3 May 2024 17:28:27 +0200 Subject: [PATCH 3/7] add /api/hs/hosting/asset-types and /api/hs/hosting/asset-types/{assetType} endpoints --- .../asset/HsHostingAssetPropsController.java | 42 +++++ .../HsHostingAssetPropertyValidator.java | 24 +++ .../validator/HsHostingAssetValidator.java | 24 +++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 19 +-- .../hs-hosting-asset-types-props.yaml | 26 +++ .../hs-hosting/hs-hosting-asset-types.yaml | 19 +++ .../hs-hosting/hs-hosting-assets.yaml | 4 +- .../api-definition/hs-hosting/hs-hosting.yaml | 8 +- ...ingAssetPropsControllerAcceptanceTest.java | 156 ++++++++++++++++++ 9 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java create mode 100644 src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml create mode 100644 src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java new file mode 100644 index 00000000..e15b906d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -0,0 +1,42 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator; +import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi; +import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + + +@RestController +public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { + + @Override + public ResponseEntity> listAssetTypes() { + final var resource = HsHostingAssetValidator.types().stream() + .map(Enum::name) + .toList(); + return ResponseEntity.ok(resource); + } + + @Override + public ResponseEntity> listAssetTypeProps( + final HsHostingAssetTypeResource assetType) { + + final var propValidators = HsHostingAssetValidator.forType(HsHostingAssetType.of(assetType)); +// final List> resource = propValidators.properties() +// .stream() +// .map(HsHostingAssetPropsController::simplifyTypeProperty) +// .toList(); + final List> resource = propValidators.properties(); + return ResponseEntity.ok((List)resource); + } + +// private static Map simplifyTypeProperty(Map source) { +// final var target = new LinkedHashMap<>(source); +// target.put("type", source,) +// return target; +// } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java index 71a1ec13..0dea3e77 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validator; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; @@ -59,6 +60,14 @@ public abstract class HsHostingAssetPropertyValidator { throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); } } + + public Map toMap(final ObjectMapper mapper) { + final Map map = mapper.convertValue(this, Map.class); + map.put("type", simpleTypeName()); + return map; + } + + protected abstract String simpleTypeName(); } @Setter @@ -90,6 +99,11 @@ class IntegerPropertyValidator extends HsHostingAssetPropertyValidator{ result.add("'" + propertyName + "' is expected to be multiple of " + step + " but is " + propValue); } } + + @Override + protected String simpleTypeName() { + return "integer"; + } } @Setter @@ -116,6 +130,11 @@ class EnumPropertyValidator extends HsHostingAssetPropertyValidator { result.add("'" + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); } } + + @Override + protected String simpleTypeName() { + return "enumeration"; + } } @Setter @@ -145,4 +164,9 @@ class BooleanPropertyValidator extends HsHostingAssetPropertyValidator } } } + + @Override + protected String simpleTypeName() { + return "boolean"; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java index 26d4c5e4..b31cd68d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java @@ -1,11 +1,16 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validator; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import static java.util.Arrays.stream; import static net.hostsharing.hsadminng.hs.hosting.asset.validator.EnumPropertyValidator.enumProperty; @@ -60,6 +65,10 @@ public class HsHostingAssetValidator { propertyValidators = validators; } + public static Set types() { + return validators.keySet(); + } + public List validate(final HsHostingAssetEntity assetEntity) { final var result = new ArrayList(); assetEntity.getConfig().keySet().forEach( givenPropName -> { @@ -72,4 +81,19 @@ public class HsHostingAssetValidator { }); return result; } + + public List> properties() { + final var mapper = new ObjectMapper(); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + return Arrays.stream(propertyValidators) + .map(propertyValidator -> propertyValidator.toMap(mapper)) + .map(HsHostingAssetValidator::asKeyValueMap) + .toList(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Map asKeyValueMap(final Map map) { + return (Map) map; + } + } diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index f3ecb6a3..9f33b45d 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -80,18 +80,11 @@ components: # forces generating a java.lang.Object containing a Map, instead of class AssetConfiguration anyOf: - type: object - properties: - CPU: - type: integer - minimum: 1 - maximum: 16 - SSD: - type: integer - minimum: 16 - maximum: 4096 - HDD: - type: integer - minimum: 16 - maximum: 4096 additionalProperties: true + HsHostingAssetProps: + # FIXME: add proper schema spec + anyOf: + - type: object + additionalProperties: true + diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml new file mode 100644 index 00000000..c7723c22 --- /dev/null +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types-props.yaml @@ -0,0 +1,26 @@ +get: + summary: Returns a list of available asset properties for the given type. + description: Returns the list of available properties and their validations for a given asset type. + tags: + - hs-hosting-asset-props + operationId: listAssetTypeProps + parameters: + - name: assetType + in: path + required: true + schema: + $ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType' + description: The asset type whose properties are to be returned. + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + $ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetProps' + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml new file mode 100644 index 00000000..f1ab17e0 --- /dev/null +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-types.yaml @@ -0,0 +1,19 @@ +get: + summary: Returns a list of available asset types. + description: Returns the list of asset types to enable an adaptive UI. + tags: + - hs-hosting-asset-props + operationId: listAssetTypes + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + type: string + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml index 8b81ecc7..a08a36a1 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml @@ -13,18 +13,20 @@ get: schema: type: string format: uuid + description: The UUID of the debitor, whose hosting assets are to be listed. - name: parentAssetUuid in: query required: false schema: type: string format: uuid + description: The UUID of the parentAsset, whose hosting assets are to be listed. - name: type in: query required: false schema: $ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType' - description: The UUID of the debitor, whose hosting assets are to be listed. + description: The type of hosting assets to be listed. responses: "200": description: OK diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml index 4f8f29d5..e83b8436 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml @@ -8,10 +8,16 @@ servers: paths: - # Items + # Assets /api/hs/hosting/assets: $ref: "hs-hosting-assets.yaml" /api/hs/hosting/assets/{assetUuid}: $ref: "hs-hosting-assets-with-uuid.yaml" + + /api/hs/hosting/asset-types: + $ref: "hs-hosting-asset-types.yaml" + + /api/hs/hosting/asset-types/{assetType}: + $ref: "hs-hosting-asset-types-props.yaml" diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java new file mode 100644 index 00000000..92f7e2b8 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -0,0 +1,156 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import io.restassured.RestAssured; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { HsadminNgApplication.class, JpaAttempt.class } +) +class HsHostingAssetPropsControllerAcceptanceTest { + + @LocalServerPort + private Integer port; + + @Test + void anyone_canListAvailableAssetTypes() { + + RestAssured // @formatter:off + .given() + .port(port) + .when() + .get("http://localhost/api/hs/hosting/asset-types") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + "MANAGED_SERVER", + "MANAGED_WEBSPACE", + "CLOUD_SERVER" + ] + """)); + // @formatter:on + } + + @Test + void globalAdmin_canListPropertiesOfGivenAssetType() { + + RestAssured // @formatter:off + .given() + .port(port) + .when() + .get("http://localhost/api/hs/hosting/asset-types/" + HsHostingAssetType.MANAGED_SERVER) + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "type": "integer", + "propertyName": "CPUs", + "required": true, + "unit": null, + "min": 1, + "max": 32, + "step": null + }, + { + "type": "integer", + "propertyName": "RAM", + "required": true, + "unit": "GB", + "min": 1, + "max": 128, + "step": null + }, + { + "type": "integer", + "propertyName": "SSD", + "required": true, + "unit": "GB", + "min": 25, + "max": 1000, + "step": 25 + }, + { + "type": "integer", + "propertyName": "HDD", + "required": false, + "unit": "GB", + "min": 0, + "max": 4000, + "step": 250 + }, + { + "type": "integer", + "propertyName": "Traffic", + "required": true, + "unit": "GB", + "min": 250, + "max": 10000, + "step": 250 + }, + { + "type": "enumeration", + "propertyName": "SLA-Platform", + "required": false, + "values": [ + "BASIC", + "EXT8H", + "EXT4H", + "EXT2H" + ] + }, + { + "type": "boolean", + "propertyName": "SLA-EMail", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-Maria", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-PgSQL", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-Office", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-Web", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + } + ] + """)); + // @formatter:on + } + +} -- 2.39.5 From 505052e8380c503c36b10191a019732ce495793d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 5 May 2024 15:12:31 +0200 Subject: [PATCH 4/7] rename enumProperty to enumerationProperty --- .../asset/validator/HsHostingAssetPropertyValidator.java | 2 +- .../hosting/asset/validator/HsHostingAssetValidator.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java index 0dea3e77..7e61845f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetPropertyValidator.java @@ -115,7 +115,7 @@ class EnumPropertyValidator extends HsHostingAssetPropertyValidator { super(String.class, propertyName); } - public static EnumPropertyValidator enumProperty(final String propertyName) { + public static EnumPropertyValidator enumerationProperty(final String propertyName) { return new EnumPropertyValidator(propertyName); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java index b31cd68d..1389de21 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validator/HsHostingAssetValidator.java @@ -13,7 +13,7 @@ import java.util.Map; import java.util.Set; import static java.util.Arrays.stream; -import static net.hostsharing.hsadminng.hs.hosting.asset.validator.EnumPropertyValidator.enumProperty; +import static net.hostsharing.hsadminng.hs.hosting.asset.validator.EnumPropertyValidator.enumerationProperty; import static net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetPropertyValidator.defType; import static net.hostsharing.hsadminng.hs.hosting.asset.validator.BooleanPropertyValidator.booleanProperty; import static net.hostsharing.hsadminng.hs.hosting.asset.validator.IntegerPropertyValidator.integerProperty; @@ -27,14 +27,14 @@ public class HsHostingAssetValidator { integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), - enumProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional())), + enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional())), defType(HsHostingAssetType.MANAGED_SERVER, new HsHostingAssetValidator( integerProperty("CPUs").min(1).max(32).required(), integerProperty("RAM").unit("GB").min(1).max(128).required(), integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), - enumProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(), + enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(), booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(), booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(), booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(), @@ -44,7 +44,7 @@ public class HsHostingAssetValidator { integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(), integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(), integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(), - enumProperty("SLA-Platform").values("BASIC", "EXT24H").optional(), + enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(), integerProperty("Daemons").min(0).max(10).optional(), booleanProperty("Online Office Server").optional()) )); -- 2.39.5 From ad6010061480dad71f516df65b2472ea11dfdf6f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 5 May 2024 15:13:21 +0200 Subject: [PATCH 5/7] more detailed OpenAPI spec for HsHostingAssetProps --- .../hs-hosting/hs-hosting-asset-schemas.yaml | 84 +++++++++++++++++-- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index 9f33b45d..59696a23 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -80,11 +80,85 @@ components: # forces generating a java.lang.Object containing a Map, instead of class AssetConfiguration anyOf: - type: object + # single source of supported properties just via /api/hs/hosting/asset-types/{assetType} + # TODO.impl: later, we could generate the config types and their properties from the validation config additionalProperties: true - HsHostingAssetProps: - # FIXME: add proper schema spec - anyOf: - - type: object - additionalProperties: true + PropertyDescriptor: + type: object + properties: + "type": + type: string + enum: + - integer + - boolean + - enumeration + "propertyName": + type: string + pattern: "^[ a-zA-Z0-9_-]$" + "required": + type: boolean + required: + - type + - propertyName + - required + IntegerPropertyDescriptor: + allOf: + - $ref: '#/components/schemas/PropertyDescriptor' + - type: object + properties: + "type": + type: string + enum: + - integer + "unit": + type: string + "min": + type: integer + minimum: 0 + "max": + type: integer + minimum: 0 + "step": + type: integer + minimum: 1 + required: + - "type" + - "propertyName" + - "required" + + BooleanPropertyDescriptor: + allOf: + - $ref: '#/components/schemas/PropertyDescriptor' + - type: object + properties: + "type": + type: string + enum: + - boolean + "falseIf": + type: object + anyOf: + - type: object + additionalProperties: true + + EnumerationPropertyDescriptor: + allOf: + - $ref: '#/components/schemas/PropertyDescriptor' + - type: object + properties: + "type": + type: string + enum: + - enumeration + "values": + type: array + items: + type: string + + HsHostingAssetProps: + anyOf: + - $ref: '#/components/schemas/IntegerPropertyDescriptor' + - $ref: '#/components/schemas/BooleanPropertyDescriptor' + - $ref: '#/components/schemas/EnumerationPropertyDescriptor' -- 2.39.5 From f4a7c80e7a0f21d7d23256c1e390e264b8bad34e Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 5 May 2024 15:22:16 +0200 Subject: [PATCH 6/7] cleanup after self-review --- .../asset/HsHostingAssetPropsController.java | 17 +++++++---------- .../api-definition/hs-hosting/hs-hosting.yaml | 2 ++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java index e15b906d..8a3f1523 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -26,17 +26,14 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { final HsHostingAssetTypeResource assetType) { final var propValidators = HsHostingAssetValidator.forType(HsHostingAssetType.of(assetType)); -// final List> resource = propValidators.properties() -// .stream() -// .map(HsHostingAssetPropsController::simplifyTypeProperty) -// .toList(); final List> resource = propValidators.properties(); - return ResponseEntity.ok((List)resource); + return ResponseEntity.ok(toListOfObjects(resource)); } -// private static Map simplifyTypeProperty(Map source) { -// final var target = new LinkedHashMap<>(source); -// target.put("type", source,) -// return target; -// } + private List toListOfObjects(final List> resource) { + // OpenApi ony generates List not List> for the Java interface. + // But Spring properly converts the List of Maps, thus we can simply cast the type: + //noinspection rawtypes,unchecked + return (List) resource; + } } diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml index e83b8436..b0df69dc 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml @@ -16,6 +16,8 @@ paths: /api/hs/hosting/assets/{assetUuid}: $ref: "hs-hosting-assets-with-uuid.yaml" + # Asset-Types + /api/hs/hosting/asset-types: $ref: "hs-hosting-asset-types.yaml" -- 2.39.5 From 358a0c7b3acf21473a7086f1ac0b51e85050c630 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 5 May 2024 19:21:08 +0200 Subject: [PATCH 7/7] formatting --- ...ingAssetPropsControllerAcceptanceTest.java | 228 +++++++++--------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 92f7e2b8..58c7bf91 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -23,19 +23,19 @@ class HsHostingAssetPropsControllerAcceptanceTest { RestAssured // @formatter:off .given() - .port(port) + .port(port) .when() - .get("http://localhost/api/hs/hosting/asset-types") + .get("http://localhost/api/hs/hosting/asset-types") .then().log().all().assertThat() - .statusCode(200) - .contentType("application/json") - .body("", lenientlyEquals(""" - [ - "MANAGED_SERVER", - "MANAGED_WEBSPACE", - "CLOUD_SERVER" - ] - """)); + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + "MANAGED_SERVER", + "MANAGED_WEBSPACE", + "CLOUD_SERVER" + ] + """)); // @formatter:on } @@ -44,112 +44,112 @@ class HsHostingAssetPropsControllerAcceptanceTest { RestAssured // @formatter:off .given() - .port(port) + .port(port) .when() - .get("http://localhost/api/hs/hosting/asset-types/" + HsHostingAssetType.MANAGED_SERVER) + .get("http://localhost/api/hs/hosting/asset-types/" + HsHostingAssetType.MANAGED_SERVER) .then().log().all().assertThat() - .statusCode(200) - .contentType("application/json") - .body("", lenientlyEquals(""" - [ - { - "type": "integer", - "propertyName": "CPUs", - "required": true, - "unit": null, - "min": 1, - "max": 32, - "step": null - }, - { - "type": "integer", - "propertyName": "RAM", - "required": true, - "unit": "GB", - "min": 1, - "max": 128, - "step": null - }, - { - "type": "integer", - "propertyName": "SSD", - "required": true, - "unit": "GB", - "min": 25, - "max": 1000, - "step": 25 - }, - { - "type": "integer", - "propertyName": "HDD", - "required": false, - "unit": "GB", - "min": 0, - "max": 4000, - "step": 250 - }, - { - "type": "integer", - "propertyName": "Traffic", - "required": true, - "unit": "GB", - "min": 250, - "max": 10000, - "step": 250 - }, - { - "type": "enumeration", - "propertyName": "SLA-Platform", - "required": false, - "values": [ - "BASIC", - "EXT8H", - "EXT4H", - "EXT2H" - ] - }, - { - "type": "boolean", - "propertyName": "SLA-EMail", - "required": false, - "falseIf": { - "SLA-Platform": "BASIC" - } - }, - { - "type": "boolean", - "propertyName": "SLA-Maria", - "required": false, - "falseIf": { - "SLA-Platform": "BASIC" - } - }, - { - "type": "boolean", - "propertyName": "SLA-PgSQL", - "required": false, - "falseIf": { - "SLA-Platform": "BASIC" - } - }, - { - "type": "boolean", - "propertyName": "SLA-Office", - "required": false, - "falseIf": { - "SLA-Platform": "BASIC" - } - }, - { - "type": "boolean", - "propertyName": "SLA-Web", - "required": false, - "falseIf": { - "SLA-Platform": "BASIC" + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "type": "integer", + "propertyName": "CPUs", + "required": true, + "unit": null, + "min": 1, + "max": 32, + "step": null + }, + { + "type": "integer", + "propertyName": "RAM", + "required": true, + "unit": "GB", + "min": 1, + "max": 128, + "step": null + }, + { + "type": "integer", + "propertyName": "SSD", + "required": true, + "unit": "GB", + "min": 25, + "max": 1000, + "step": 25 + }, + { + "type": "integer", + "propertyName": "HDD", + "required": false, + "unit": "GB", + "min": 0, + "max": 4000, + "step": 250 + }, + { + "type": "integer", + "propertyName": "Traffic", + "required": true, + "unit": "GB", + "min": 250, + "max": 10000, + "step": 250 + }, + { + "type": "enumeration", + "propertyName": "SLA-Platform", + "required": false, + "values": [ + "BASIC", + "EXT8H", + "EXT4H", + "EXT2H" + ] + }, + { + "type": "boolean", + "propertyName": "SLA-EMail", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-Maria", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-PgSQL", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-Office", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } + }, + { + "type": "boolean", + "propertyName": "SLA-Web", + "required": false, + "falseIf": { + "SLA-Platform": "BASIC" + } } - } - ] - """)); + ] + """)); // @formatter:on } -- 2.39.5