hierarchical-validation-baseline (#59)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #59
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-06-14 16:48:00 +02:00
parent fc2b437a55
commit 46dc653174
70 changed files with 1620 additions and 694 deletions

View File

@ -9,7 +9,7 @@ import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
@Getter
class CustomErrorResponse {
public class CustomErrorResponse {
static ResponseEntity<CustomErrorResponse> errorResponse(
final WebRequest request,

View File

@ -0,0 +1,19 @@
package net.hostsharing.hsadminng.errors;
import jakarta.validation.ValidationException;
import java.util.List;
import static java.lang.String.join;
public class MultiValidationException extends ValidationException {
private MultiValidationException(final List<String> violations) {
super("[\n" + join(",\n", violations) + "\n]");
}
public static void throwInvalid(final List<String> violations) {
if (!violations.isEmpty()) {
throw new MultiValidationException(violations);
}
}
}

View File

@ -73,9 +73,10 @@ public class RestResponseEntityExceptionHandler
}
@ExceptionHandler({ Iban4jException.class, ValidationException.class })
protected ResponseEntity<CustomErrorResponse> handleIbanAndBicExceptions(
protected ResponseEntity<CustomErrorResponse> handleValidationExceptions(
final Throwable exc, final WebRequest request) {
final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0);
final String fullMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage();
final var message = exc instanceof MultiValidationException ? fullMessage : line(fullMessage, 0);
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
}

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
@ -13,11 +14,12 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController
@ -32,6 +34,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Autowired
private HsBookingItemRepository bookingItemRepo;
@PersistenceContext
private EntityManager em;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByProjectUuid(
@ -57,7 +62,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = bookingItemRepo.save(valid(entityToSave));
final var saved = HsBookingItemEntityValidatorRegistry.validated(bookingItemRepo.save(entityToSave));
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -78,6 +83,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
context.define(currentUser, assumedRoles);
final var result = bookingItemRepo.findByUuid(bookingItemUuid);
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
return result
.map(bookingItemEntity -> ResponseEntity.ok(
mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
@ -112,7 +118,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
new HsBookingItemEntityPatcher(current).apply(body);
final var saved = bookingItemRepo.save(valid(current));
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(current));
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}

View File

@ -10,7 +10,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -19,6 +19,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
@ -27,12 +28,14 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -62,7 +65,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable<HsBookingItemEntity, HsBookingItemType> {
public class HsBookingItemEntity implements Stringifyable, RbacObject {
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
.withProp(HsBookingItemEntity::getProject)
@ -105,6 +108,14 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
@Column(columnDefinition = "resources")
private Map<String, Object> resources = new HashMap<>();
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
@JoinColumn(name="parentitemuuid", referencedColumnName="uuid")
private List<HsBookingItemEntity> subBookingItems;
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
@JoinColumn(name="bookingitemuuid", referencedColumnName="uuid")
private List<HsHostingAssetEntity> subHostingAssets;
@Transient
private PatchableMapWrapper<Object> resourcesWrapper;
@ -150,16 +161,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
return parentItem == null ? null : parentItem.relatedProject();
}
@Override
public String getPropertiesName() {
return "resources";
}
@Override
public Map<String, Object> getProperties() {
return resources;
}
public HsBookingProjectEntity getRelatedProject() {
return project != null ? project : parentItem.getRelatedProject();
}

View File

@ -0,0 +1,77 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingItemEntity> {
public HsBookingItemEntityValidator(final ValidatableProperty<?>... properties) {
super(properties);
}
public List<String> validate(final HsBookingItemEntity bookingItem) {
return sequentiallyValidate(
() -> validateProperties(bookingItem),
() -> optionallyValidate(bookingItem.getParentItem()),
() -> validateAgainstSubEntities(bookingItem)
);
}
private List<String> validateProperties(final HsBookingItemEntity bookingItem) {
return enrich(prefix(bookingItem.toShortString(), "resources"), validateProperties(bookingItem.getResources()));
}
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
return bookingItem != null
? enrich(prefix(bookingItem.toShortString(), ""),
HsBookingItemEntityValidatorRegistry.doValidate(bookingItem))
: emptyList();
}
protected List<String> validateAgainstSubEntities(final HsBookingItemEntity bookingItem) {
return enrich(prefix(bookingItem.toShortString(), "resources"),
Stream.concat(
stream(propertyValidators)
.map(propDef -> propDef.validateTotals(bookingItem))
.flatMap(Collection::stream),
stream(propertyValidators)
.filter(ValidatableProperty::isTotalsValidator)
.map(prop -> validateMaxTotalValue(bookingItem, prop))
).filter(Objects::nonNull).toList());
}
// TODO.refa: convert into generic shape like multi-options validator
private static String validateMaxTotalValue(
final HsBookingItemEntity bookingItem,
final ValidatableProperty<?> propDef) {
final var propName = propDef.propertyName();
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
.stream()
.map(subItem -> propDef.getValue(subItem.getResources()))
.map(HsBookingItemEntityValidator::toNonNullInteger)
.reduce(0, Integer::sum);
final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources());
if (propDef.thresholdPercentage() != null ) {
return totalValue > (maxValue * propDef.thresholdPercentage() / 100)
? "%s' maximum total is %d%s, but actual total %s %d%s, which exceeds threshold of %d%%"
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage())
: null;
} else {
return totalValue > maxValue
? "%s' maximum total is %d%s, but actual total %s %d%s"
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit)
: null;
}
}
}

View File

@ -1,12 +1,12 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -14,37 +14,42 @@ import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD;
@UtilityClass
public class HsBookingItemEntityValidators {
public class HsBookingItemEntityValidatorRegistry {
private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity, HsBookingItemType>> validators = new HashMap<>();
private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity>> validators = new HashMap<>();
static {
register(PRIVATE_CLOUD, new HsPrivateCloudBookingItemValidator());
register(CLOUD_SERVER, new HsCloudServerBookingItemValidator());
register(MANAGED_SERVER, new HsManagedServerBookingItemValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceBookingItemValidator());
}
private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity, HsBookingItemType> validator) {
private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsBookingItemEntity, HsBookingItemType> forType(final Enum<HsBookingItemType> type) {
return validators.get(type);
public static HsEntityValidator<HsBookingItemEntity> forType(final Enum<HsBookingItemType> type) {
if ( validators.containsKey(type)) {
return validators.get(type);
}
throw new IllegalArgumentException("no validator found for type " + type);
}
public static Set<Enum<HsBookingItemType>> types() {
return validators.keySet();
}
public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) {
final var violations = HsBookingItemEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
public static List<String> doValidate(final HsBookingItemEntity bookingItem) {
return HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validate(bookingItem);
}
public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) {
MultiValidationException.throwInvalid(doValidate(entityToSave));
return entityToSave;
}
}

View File

@ -1,20 +1,18 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsCloudServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
HsCloudServerBookingItemValidator() {
super(
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("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
);

View File

@ -1,24 +1,22 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator {
HsManagedServerBookingItemValidator() {
super(
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(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required().asTotalLimit().withThreshold(200),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0).asTotalLimit().withThreshold(200),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required().asTotalLimit().withThreshold(200),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),

View File

@ -1,24 +1,98 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.IntegerProperty;
import org.apache.commons.lang3.function.TriFunction;
import java.util.List;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
import static java.util.Collections.emptyList;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_DATABASE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_USER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedWebspaceBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator {
public HsManagedWebspaceBookingItemValidator() {
super(
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(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
integerProperty("Daemons").min(0).max(10).optional(),
booleanProperty("Online Office Server").optional()
integerProperty("Multi").min(1).max(100).step(1).withDefault(1)
.eachComprising( 25, unixUsers())
.eachComprising( 5, databaseUsers())
.eachComprising( 5, databases())
.eachComprising(250, eMailAddresses()),
integerProperty("Daemons").min(0).max(10).withDefault(0),
booleanProperty("Online Office Server").optional(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").withDefault("BASIC")
);
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> unixUsers() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var unixUserCount = entity.getSubHostingAssets().stream()
.flatMap(ha -> ha.getSubHostingAssets().stream())
.filter(ha -> ha.getType() == UNIX_USER)
.count();
final long limitingValue = prop.getValue(entity.getResources());
if (unixUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " unix users, but " + unixUserCount + " found");
}
return emptyList();
};
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> databaseUsers() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var unixUserCount = entity.getSubHostingAssets().stream()
.flatMap(ha -> ha.getSubHostingAssets().stream())
.filter(bi -> bi.getType() == PGSQL_USER || bi.getType() == MARIADB_USER )
.count();
final long limitingValue = prop.getValue(entity.getResources());
if (unixUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " database users, but " + unixUserCount + " found");
}
return emptyList();
};
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> databases() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var unixUserCount = entity.getSubHostingAssets().stream()
.flatMap(ha -> ha.getSubHostingAssets().stream())
.filter(bi -> bi.getType()==PGSQL_USER || bi.getType()==MARIADB_USER )
.flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream()
.filter(ha -> ha.getType()==PGSQL_DATABASE || ha.getType()==MARIADB_DATABASE))
.count();
final long limitingValue = prop.getValue(entity.getResources());
if (unixUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found");
}
return emptyList();
};
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> eMailAddresses() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var unixUserCount = entity.getSubHostingAssets().stream()
.flatMap(ha -> ha.getSubHostingAssets().stream())
.filter(bi -> bi.getType() == DOMAIN_EMAIL_SETUP)
.flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream()
.filter(ha -> ha.getType()==EMAIL_ADDRESS))
.count();
final long limitingValue = prop.getValue(entity.getResources());
if (unixUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found");
}
return emptyList();
};
}
}

View File

@ -0,0 +1,18 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
HsPrivateCloudBookingItemValidator() {
super(
integerProperty("CPUs").min(4).max(128).required().asTotalLimit(),
integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(),
integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(),
integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(),
integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC")
);
}
}

View File

@ -20,7 +20,7 @@ import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated;
@RestController
public class HsHostingAssetController implements HsHostingAssetsApi {
@ -62,7 +62,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = assetRepo.save(valid(entityToSave));
final var saved = validated(assetRepo.save(entityToSave));
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -117,7 +117,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
new HsHostingAssetEntityPatcher(current).apply(body);
final var saved = assetRepo.save(valid(current));
final var saved = validated(assetRepo.save(current));
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
return ResponseEntity.ok(mapped);
}

View File

@ -8,7 +8,6 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -17,6 +16,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
@ -25,11 +25,13 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -56,7 +58,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validatable<HsHostingAssetEntity, HsHostingAssetType> {
public class HsHostingAssetEntity implements Stringifyable, RbacObject {
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
.withProp(HsHostingAssetEntity::getType)
@ -91,6 +93,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata
@Enumerated(EnumType.STRING)
private HsHostingAssetType type;
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
@JoinColumn(name="parentassetuuid", referencedColumnName="uuid")
private List<HsHostingAssetEntity> subHostingAssets;
@Column(name = "identifier")
private String identifier; // vm1234, xyz00, example.org, xyz00_abc
@ -114,16 +120,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
}
@Override
public String getPropertiesName() {
return "config";
}
@Override
public Map<String, Object> getProperties() {
return config;
}
@Override
public String toString() {
return stringify.apply(this);

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry;
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;
@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override
public ResponseEntity<List<String>> listAssetTypes() {
final var resource = HsHostingAssetEntityValidators.types().stream()
final var resource = HsHostingAssetEntityValidatorRegistry.types().stream()
.map(Enum::name)
.toList();
return ResponseEntity.ok(resource);
@ -25,7 +25,8 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
public ResponseEntity<List<Object>> listAssetTypeProps(
final HsHostingAssetTypeResource assetType) {
final var propValidators = HsHostingAssetEntityValidators.forType(HsHostingAssetType.of(assetType));
final Enum<HsHostingAssetType> type = HsHostingAssetType.of(assetType);
final var propValidators = HsHostingAssetEntityValidatorRegistry.forType(type);
final List<Map<String, Object>> resource = propValidators.properties();
return ResponseEntity.ok(toListOfObjects(resource));
}

View File

@ -0,0 +1,76 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
import java.util.List;
import java.util.Objects;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
public HsHostingAssetEntityValidator(final ValidatableProperty<?>... properties) {
super(properties);
}
@Override
public List<String> validate(final HsHostingAssetEntity assetEntity) {
return sequentiallyValidate(
() -> validateProperties(assetEntity),
() -> optionallyValidate(assetEntity.getBookingItem()),
() -> optionallyValidate(assetEntity.getParentAsset()),
() -> validateAgainstSubEntities(assetEntity)
);
}
private List<String> validateProperties(final HsHostingAssetEntity assetEntity) {
return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig()));
}
private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {
return assetEntity != null
? enrich(prefix(assetEntity.toShortString(), "parentAsset"),
HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validate(assetEntity))
: emptyList();
}
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
return bookingItem != null
? enrich(prefix(bookingItem.toShortString(), "bookingItem"),
HsBookingItemEntityValidatorRegistry.doValidate(bookingItem))
: emptyList();
}
protected List<String> validateAgainstSubEntities(final HsHostingAssetEntity assetEntity) {
return enrich(prefix(assetEntity.toShortString(), "config"),
stream(propertyValidators)
.filter(ValidatableProperty::isTotalsValidator)
.map(prop -> validateMaxTotalValue(assetEntity, prop))
.filter(Objects::nonNull)
.toList());
}
private String validateMaxTotalValue(
final HsHostingAssetEntity hostingAsset,
final ValidatableProperty<?> propDef) {
final var propName = propDef.propertyName();
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
.stream()
.map(subItem -> propDef.getValue(subItem.getConfig()))
.map(HsEntityValidator::toNonNullInteger)
.reduce(0, Integer::sum);
final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig());
return totalValue > maxValue
? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted(
propName, maxValue, propUnit, propName, totalValue, propUnit)
: null;
}
}

View File

@ -0,0 +1,50 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import java.util.*;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.*;
public class HsHostingAssetEntityValidatorRegistry {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsHostingAssetEntityValidator());
register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
register(UNIX_USER, new HsHostingAssetEntityValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsHostingAssetEntity> forType(final Enum<HsHostingAssetType> type) {
if ( validators.containsKey(type)) {
return validators.get(type);
}
throw new IllegalArgumentException("no validator found for type " + type);
}
public static Set<Enum<HsHostingAssetType>> types() {
return validators.keySet();
}
public static List<String> doValidate(final HsHostingAssetEntity hostingAsset) {
return HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()).validate(hostingAsset);
}
public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) {
MultiValidationException.throwInvalid(doValidate(entityToSave));
return entityToSave;
}
}

View File

@ -1,51 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
@UtilityClass
public class HsHostingAssetEntityValidators {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsEntityValidator<>());
register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> forType(final Enum<HsHostingAssetType> type) {
return validators.get(type);
}
public static Set<Enum<HsHostingAssetType>> types() {
return validators.keySet();
}
public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
final var violations = HsHostingAssetEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
}

View File

@ -1,12 +1,8 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator {
public HsManagedServerHostingAssetValidator() {
super(

View File

@ -1,28 +1,29 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import java.util.Collection;
import java.util.stream.Stream;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator {
public HsManagedWebspaceHostingAssetValidator() {
}
@Override
public List<String> validate(final HsHostingAssetEntity assetEntity) {
final var result = super.validate(assetEntity);
validateIdentifierPattern(result, assetEntity);
return result;
return Stream.of(validateIdentifierPattern(assetEntity), super.validate(assetEntity))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private static void validateIdentifierPattern(final List<String> result, final HsHostingAssetEntity assetEntity) {
private static List<String> validateIdentifierPattern(final HsHostingAssetEntity assetEntity) {
final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix() + "[0-9][0-9]$";
if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) {
result.add("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
}
return Collections.emptyList();
}
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
@ -13,14 +14,12 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ValidationException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static java.lang.String.join;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
@RestController
@ -97,9 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
validateDebitTransaction(requestBody, violations);
validateCreditTransaction(requestBody, violations);
validateAssetValue(requestBody, violations);
if (violations.size() > 0) {
throw new ValidationException("[" + join(", ", violations) + "]");
}
MultiValidationException.throwInvalid(violations);
}
private static void validateDebitTransaction(

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
@ -14,14 +15,12 @@ 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.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static java.lang.String.join;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
@ -99,9 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
validateSubscriptionTransaction(requestBody, violations);
validateCancellationTransaction(requestBody, violations);
validateshareCount(requestBody, violations);
if (violations.size() > 0) {
throw new ValidationException("[" + join(", ", violations) + "]");
}
MultiValidationException.throwInvalid(violations);
}
private static void validateSubscriptionTransaction(

View File

@ -0,0 +1,46 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
@Setter
public class BooleanProperty extends ValidatableProperty<Boolean> {
private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL);
private Map.Entry<String, String> falseIf;
private BooleanProperty(final String propertyName) {
super(Boolean.class, propertyName, KEY_ORDER);
}
public static BooleanProperty booleanProperty(final String propertyName) {
return new BooleanProperty(propertyName);
}
public ValidatableProperty<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final Boolean propValue, final Map<String, Object> props) {
if (falseIf != null && propValue) {
final Object referencedValue = props.get(falseIf.getKey());
if (Objects.equals(referencedValue, falseIf.getValue())) {
result.add(propertyName + "' is expected to be false because " +
falseIf.getKey() + "=" + referencedValue + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -1,42 +0,0 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
@Setter
public class BooleanPropertyValidator extends HsPropertyValidator<Boolean> {
private Map.Entry<String, String> falseIf;
private BooleanPropertyValidator(final String propertyName) {
super(Boolean.class, propertyName);
}
public static BooleanPropertyValidator booleanProperty(final String propertyName) {
return new BooleanPropertyValidator(propertyName);
}
public HsPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final Boolean propValue, final Map<String, Object> props) {
if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
if (propValue) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be false because " +
propertiesName+"." + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -0,0 +1,44 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
@Setter
public class EnumerationProperty extends ValidatableProperty<String> {
private static final String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD,
Array.of("values"),
ValidatableProperty.KEY_ORDER_TAIL);
private String[] values;
private EnumerationProperty(final String propertyName) {
super(String.class, propertyName, KEY_ORDER);
}
public static EnumerationProperty enumerationProperty(final String propertyName) {
return new EnumerationProperty(propertyName);
}
public ValidatableProperty<String> values(final String... values) {
this.values = values;
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> 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 + "'");
}
}
@Override
protected String simpleTypeName() {
return "enumeration";
}
}

View File

@ -1,38 +0,0 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
@Setter
public class EnumerationPropertyValidator extends HsPropertyValidator<String> {
private String[] values;
private EnumerationPropertyValidator(final String propertyName) {
super(String.class, propertyName);
}
public static EnumerationPropertyValidator enumerationProperty(final String propertyName) {
return new EnumerationPropertyValidator(propertyName);
}
public HsPropertyValidator<String> values(final String... values) {
this.values = values;
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final String propValue, final Map<String, Object> props) {
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}