refactoring for implicit creation of dependend hosting-assets (#108)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: #108 Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
parent
d949604d70
commit
cb4aecb9c8
27
README.md
27
README.md
@ -550,12 +550,37 @@ Dependency versions can be automatically upgraded to the latest available versio
|
||||
gw useLatestVersions
|
||||
```
|
||||
|
||||
Afterwards, `gw check` is automatically started.
|
||||
Afterward, `gw check` is automatically started.
|
||||
Please only commit+push to master if the check run shows no errors.
|
||||
|
||||
More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin).
|
||||
|
||||
|
||||
## Biggest Flaws in our Architecture
|
||||
|
||||
### The RBAC System is too Complicated
|
||||
|
||||
Now, where we have a better experience with what we really need from the RBAC system, we have learned
|
||||
that and creates too many (grant- and role-) rows and too even tables which could be avoided completely.
|
||||
|
||||
The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC,
|
||||
e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER.
|
||||
Grants between these for the same DB-row would be implicit by order comparision.
|
||||
This way we would get rid of all explicit grants within the same DB-row
|
||||
and would not need the `rbac.role` table anymore.
|
||||
We would also reduce the depth of the expensive recursive CTE-query.
|
||||
|
||||
This has to be explored further.
|
||||
For now, we just keep it in mind and
|
||||
|
||||
### The Mapper is Error-Prone
|
||||
|
||||
Where `org.modelmapper.ModelMapper` reduces bloat-code a lot and has some nice features about recursive data-structure mappings,
|
||||
it often causes strange errors which are hard to fix.
|
||||
E.g. the uuid of the target main object is often taken from an uuid of a sub-subject.
|
||||
(For now, use `StrictMapper` to avoid this, for the case it happens.)
|
||||
|
||||
|
||||
## How To ...
|
||||
|
||||
### How to Configure .pgpass for the Default PostgreSQL Database?
|
||||
|
@ -9,15 +9,25 @@ import java.lang.annotation.Target;
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface DisplayAs {
|
||||
|
||||
class DisplayName {
|
||||
|
||||
public static String of(final Class<?> clazz) {
|
||||
final var displayNameAnnot = clazz.getAnnotation(DisplayAs.class);
|
||||
final var displayNameAnnot = getDisplayNameAnnotation(clazz);
|
||||
return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName();
|
||||
}
|
||||
|
||||
public static String of(@NotNull final Object instance) {
|
||||
return of(instance.getClass());
|
||||
}
|
||||
|
||||
private static DisplayAs getDisplayNameAnnotation(final Class<?> clazz) {
|
||||
if (clazz == null) {
|
||||
return null;
|
||||
}
|
||||
final var annot = clazz.getAnnotation(DisplayAs.class);
|
||||
return annot != null ? annot : getDisplayNameAnnotation(clazz.getSuperclass());
|
||||
}
|
||||
}
|
||||
|
||||
String value() default "";
|
||||
|
@ -5,17 +5,18 @@ 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.BookingItemEntitySaveProcessor;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@ -30,13 +31,13 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsBookingItemRbacRepository bookingItemRepo;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
@Autowired
|
||||
private EntityManagerWrapper em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@ -48,7 +49,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
|
||||
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@ -62,15 +63,20 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
context.define(currentSubject, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
|
||||
final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave));
|
||||
final var mapped = new BookingItemEntitySaveProcessor(em, entityToSave)
|
||||
.preprocessEntity()
|
||||
.validateEntity()
|
||||
.prepareForSave()
|
||||
.save()
|
||||
.validateContext()
|
||||
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||
.revampProperties();
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/hs/booking/items/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.buildAndExpand(mapped.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@ -87,7 +93,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
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)))
|
||||
mapper.map(bookingItemEntity, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@ -120,18 +126,21 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
||||
new HsBookingItemEntityPatcher(current).apply(body);
|
||||
|
||||
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current));
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setValidFrom(entity.getValidity().lower());
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||
}
|
||||
};
|
||||
|
||||
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> RBAC_ENTITY_TO_RESOURCE_POSTMAPPER = ITEM_TO_RESOURCE_POSTMAPPER::accept;
|
||||
|
||||
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid()));
|
||||
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
||||
entity.putResources(KeyValueMap.from(resource.getResources()));
|
||||
};
|
||||
|
@ -0,0 +1,131 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// TODO.refa: introduce common base class with HsHostingAssetEntitySaveProcessor
|
||||
/**
|
||||
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsBookingItem into a readable API.
|
||||
*/
|
||||
public class BookingItemEntitySaveProcessor {
|
||||
|
||||
private final HsEntityValidator<HsBookingItem> validator;
|
||||
private String expectedStep = "preprocessEntity";
|
||||
private final EntityManager em;
|
||||
private HsBookingItem entity;
|
||||
private HsBookingItemResource resource;
|
||||
|
||||
public BookingItemEntitySaveProcessor(final EntityManager em, final HsBookingItem entity) {
|
||||
this.em = em;
|
||||
this.entity = entity;
|
||||
this.validator = HsBookingItemEntityValidatorRegistry.forType(entity.getType());
|
||||
}
|
||||
|
||||
/// initial step allowing to set default values before any validations
|
||||
public BookingItemEntitySaveProcessor preprocessEntity() {
|
||||
step("preprocessEntity", "validateEntity");
|
||||
validator.preprocessEntity(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// validates the entity itself including its properties
|
||||
public BookingItemEntitySaveProcessor validateEntity() {
|
||||
step("validateEntity", "prepareForSave");
|
||||
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
|
||||
return this;
|
||||
}
|
||||
|
||||
// TODO.impl: remove once the migration of legacy data is done
|
||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||
step("validateEntity", "prepareForSave");
|
||||
final var ignoreRegExpPatterns = Arrays.stream(ignoreRegExp).map(Pattern::compile).toList();
|
||||
MultiValidationException.throwIfNotEmpty(
|
||||
validator.validateEntity(entity).stream()
|
||||
.filter(error -> ignoreRegExpPatterns.stream().noneMatch(p -> p.matcher(error).matches() ))
|
||||
.toList()
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// hashing passwords etc.
|
||||
public BookingItemEntitySaveProcessor prepareForSave() {
|
||||
step("prepareForSave", "save");
|
||||
validator.prepareProperties(em, entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the entity using the given `saveFunction`.
|
||||
*
|
||||
* <p>`validator.postPersist(em, entity)` is NOT called.
|
||||
* If any postprocessing is necessary, the saveFunction has to implement this.</p>
|
||||
* @param saveFunction
|
||||
* @return this
|
||||
*/
|
||||
public BookingItemEntitySaveProcessor saveUsing(final Function<HsBookingItem, HsBookingItem> saveFunction) {
|
||||
step("save", "validateContext");
|
||||
entity = saveFunction.apply(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the using the `EntityManager`, but does NOT ever merge the entity.
|
||||
*
|
||||
* <p>`validator.postPersist(em, entity)` is called afterwards with the entity guaranteed to be flushed to the database.</p>
|
||||
* @return this
|
||||
*/
|
||||
public BookingItemEntitySaveProcessor save() {
|
||||
return saveUsing(e -> {
|
||||
if (!em.contains(entity)) {
|
||||
em.persist(entity);
|
||||
}
|
||||
em.flush(); // makes RbacEntity available as RealEntity if needed
|
||||
validator.postPersist(em, entity);
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits)
|
||||
public BookingItemEntitySaveProcessor validateContext() {
|
||||
step("validateContext", "mapUsing");
|
||||
return HsEntityValidator.doWithEntityManager(em, () -> {
|
||||
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
|
||||
return this;
|
||||
});
|
||||
}
|
||||
|
||||
/// maps entity to JSON resource representation
|
||||
public BookingItemEntitySaveProcessor mapUsing(
|
||||
final Function<HsBookingItem, HsBookingItemResource> mapFunction) {
|
||||
step("mapUsing", "revampProperties");
|
||||
resource = mapFunction.apply(entity);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// removes write-only-properties and ads computed-properties
|
||||
@SuppressWarnings("unchecked")
|
||||
public HsBookingItemResource revampProperties() {
|
||||
step("revampProperties", null);
|
||||
final var revampedProps = validator.revampProperties(em, entity, (Map<String, Object>) resource.getResources());
|
||||
resource.setResources(revampedProps);
|
||||
return resource;
|
||||
}
|
||||
|
||||
// Makes sure that the steps are called in the correct order.
|
||||
// Could also be implemented using an interface per method, but that seems exaggerated.
|
||||
private void step(final String current, final String next) {
|
||||
if (!expectedStep.equals(current)) {
|
||||
throw new IllegalStateException("expected " + expectedStep + " but got " + current);
|
||||
}
|
||||
expectedStep = next;
|
||||
}
|
||||
}
|
@ -48,10 +48,11 @@ public class HsBookingItemEntityValidatorRegistry {
|
||||
}
|
||||
|
||||
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
|
||||
final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType());
|
||||
return HsEntityValidator.doWithEntityManager(em, () ->
|
||||
HsEntityValidator.sequentiallyValidate(
|
||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem),
|
||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
|
||||
() -> bookingItemValidator.validateEntity(bookingItem),
|
||||
() -> bookingItemValidator.validateContext(bookingItem))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
@ -15,6 +16,9 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
|
||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
public static final String TARGET_UNIX_USER_PROPERTY_NAME = "targetUnixUser";
|
||||
public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
|
||||
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
|
||||
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
||||
|
||||
HsDomainSetupBookingItemValidator() {
|
||||
@ -24,6 +28,12 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
||||
.notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
|
||||
.required(),
|
||||
// TODO.legacy: remove the following property once we give up legacy compatibility
|
||||
stringProperty(TARGET_UNIX_USER_PROPERTY_NAME).writeOnce()
|
||||
.maxLength(253)
|
||||
.matchesRegEx(TARGET_UNIX_USER_NAME_REGEX).describedAs("is not a valid unix-user name")
|
||||
.writeOnce()
|
||||
.required(),
|
||||
stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
|
||||
.minLength(12)
|
||||
.maxLength(64)
|
||||
|
@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjec
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -25,7 +25,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsBookingProjectRbacRepository bookingProjectRepo;
|
||||
|
@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
|
||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -35,7 +35,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsHostingAssetRbacRepository rbacAssetRepo;
|
||||
|
@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.iban4j.BicUtil;
|
||||
import org.iban4j.IbanUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -24,7 +24,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeBankAccountRepository bankAccountRepo;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
|
||||
@ -26,7 +26,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeContactRbacRepository contactRepo;
|
||||
|
@ -4,7 +4,7 @@ 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 net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||
@ -29,7 +29,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
|
||||
|
@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopShar
|
||||
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 net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||
@ -31,7 +31,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
|
||||
|
@ -7,8 +7,8 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebito
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -23,7 +23,6 @@ import jakarta.validation.ValidationException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
|
||||
@RestController
|
||||
@ -34,7 +33,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeDebitorRepository debitorRepo;
|
||||
@ -42,6 +41,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
@Autowired
|
||||
private HsOfficeRelationRealRepository relrealRepo;
|
||||
|
||||
@Autowired
|
||||
private EntityExistsValidator entityValidator;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
@ -84,10 +86,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||
if ( body.getDebitorRel() != null ) {
|
||||
body.getDebitorRel().setType(DEBITOR.name());
|
||||
final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||
validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||
validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||
validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
|
||||
} else {
|
||||
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
||||
@ -160,15 +162,4 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
// TODO.impl: extract this to some generally usable class?
|
||||
private <T extends BaseEntity<T>> T validateEntityExists(final String property, final T entitySkeleton) {
|
||||
final var foundEntity = em.find(entitySkeleton.getClass(), entitySkeleton.getUuid());
|
||||
if ( foundEntity == null) {
|
||||
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
|
||||
}
|
||||
|
||||
//noinspection unchecked
|
||||
return (T) foundEntity;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembersh
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -24,7 +24,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeMembershipRepository membershipRepo;
|
||||
|
@ -2,18 +2,18 @@ package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
||||
|
||||
private final Mapper mapper;
|
||||
private final StandardMapper mapper;
|
||||
private final HsOfficeMembershipEntity entity;
|
||||
|
||||
public HsOfficeMembershipEntityPatcher(
|
||||
final Mapper mapper,
|
||||
final StandardMapper mapper,
|
||||
final HsOfficeMembershipEntity entity) {
|
||||
this.mapper = mapper;
|
||||
this.entity = entity;
|
||||
|
@ -12,7 +12,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -36,7 +36,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePartnerRepository partnerRepo;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.office.person;
|
||||
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
|
||||
@ -23,7 +23,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePersonRepository personRepo;
|
||||
|
@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -28,7 +28,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeRelationRbacRepository relationRbacRepo;
|
||||
|
@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMand
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -28,7 +28,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeSepaMandateRepository sepaMandateRepo;
|
||||
|
@ -130,7 +130,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
||||
}
|
||||
|
||||
public Map<String, Object> revampProperties(final EntityManager em, final E entity, final Map<String, Object> config) {
|
||||
final var copy = new HashMap<>(config);
|
||||
final var copy = config != null ? new HashMap<>(config) : new HashMap();
|
||||
stream(propertyValidators).forEach(p -> {
|
||||
if (p.isWriteOnly()) {
|
||||
copy.remove(p.propertyName);
|
||||
|
@ -11,18 +11,20 @@ import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||
|
||||
/**
|
||||
* A nicer API for ModelMapper.
|
||||
*/
|
||||
public class Mapper extends ModelMapper {
|
||||
abstract class Mapper extends ModelMapper {
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
|
||||
public Mapper() {
|
||||
Mapper() {
|
||||
getConfiguration().setAmbiguityIgnored(true);
|
||||
}
|
||||
|
||||
@ -45,8 +47,12 @@ public class Mapper extends ModelMapper {
|
||||
|
||||
@Override
|
||||
public <D> D map(final Object source, final Class<D> destinationType) {
|
||||
return map("", source, destinationType);
|
||||
}
|
||||
|
||||
public <D> D map(final String namePrefix, final Object source, final Class<D> destinationType) {
|
||||
final var target = super.map(source, destinationType);
|
||||
for (Field f : destinationType.getDeclaredFields()) {
|
||||
for (Field f : getDeclaredFieldsIncludingSuperClasses(destinationType)) {
|
||||
if (f.getAnnotation(ManyToOne.class) == null) {
|
||||
continue;
|
||||
}
|
||||
@ -64,18 +70,30 @@ public class Mapper extends ModelMapper {
|
||||
if (subEntityUuid == null) {
|
||||
continue;
|
||||
}
|
||||
ReflectionUtils.setField(f, target, findEntityById(f.getType(), subEntityUuid));
|
||||
ReflectionUtils.setField(f, target, fetchEntity(namePrefix + f.getName() + ".uuid", f.getType(), subEntityUuid));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private Object findEntityById(final Class<?> entityClass, final Object subEntityUuid) {
|
||||
// using getReference would be more efficent, but results in very technical error messages
|
||||
final var entity = em.find(entityClass, subEntityUuid);
|
||||
private static <D> Field[] getDeclaredFieldsIncludingSuperClasses(final Class<D> destinationType) {
|
||||
if (destinationType == null) {
|
||||
return new Field[0];
|
||||
}
|
||||
|
||||
return Stream.concat(
|
||||
stream(destinationType.getDeclaredFields()),
|
||||
stream(getDeclaredFieldsIncludingSuperClasses(destinationType.getSuperclass())))
|
||||
.toArray(Field[]::new);
|
||||
}
|
||||
|
||||
public <E> E fetchEntity(final String propertyName, final Class<E> entityClass, final Object subEntityUuid) {
|
||||
final var entity = em.getReference(entityClass, subEntityUuid);
|
||||
if (entity != null) {
|
||||
return entity;
|
||||
}
|
||||
throw new ValidationException("Unable to find " + DisplayName.of(entityClass) + " by uuid: " + subEntityUuid);
|
||||
throw new ValidationException(
|
||||
"Unable to find " + DisplayName.of(entityClass) +
|
||||
" by " + propertyName + ": " + subEntityUuid);
|
||||
}
|
||||
|
||||
public <S, T> T map(final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
|
||||
@ -86,4 +104,13 @@ public class Mapper extends ModelMapper {
|
||||
postMapper.accept(source, target);
|
||||
return target;
|
||||
}
|
||||
|
||||
public <S, T> T map(final String namePrefix, final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
final var target = map(source, targetClass);
|
||||
postMapper.accept(source, target);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MapperConfiguration {
|
||||
|
||||
@Bean
|
||||
public Mapper modelMapper() {
|
||||
return new Mapper();
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A nicer API for ModelMapper in standard mode.
|
||||
*/
|
||||
@Component
|
||||
public class StandardMapper extends Mapper {
|
||||
|
||||
public StandardMapper() {
|
||||
getConfiguration().setAmbiguityIgnored(true);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static org.modelmapper.convention.MatchingStrategies.STRICT;
|
||||
|
||||
/**
|
||||
* A nicer API for ModelMapper in strict mode.
|
||||
*
|
||||
* <p>This makes sure that resource.whateverUuid does not accidentally get mapped to entity.uuid,
|
||||
* if resource.uuid does not exist.</p>
|
||||
*/
|
||||
@Component
|
||||
public class StrictMapper extends Mapper {
|
||||
|
||||
public StrictMapper() {
|
||||
getConfiguration().setMatchingStrategy(STRICT);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package net.hostsharing.hsadminng.persistence;
|
||||
|
||||
import net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
@Service
|
||||
public class EntityExistsValidator {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper em;
|
||||
|
||||
public <T extends BaseEntity<T>> void validateEntityExists(final String property, final T entitySkeleton) {
|
||||
final var foundEntity = em.find(entityClass(entitySkeleton), entitySkeleton.getUuid());
|
||||
if ( foundEntity == null) {
|
||||
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends BaseEntity<T>> Class<?> entityClass(final T entityOrProxy) {
|
||||
final var entityClass = entityClass(entityOrProxy.getClass());
|
||||
if (entityClass == null) {
|
||||
throw new IllegalArgumentException("@Entity not found in superclass hierarchy of " + entityOrProxy.getClass());
|
||||
}
|
||||
return entityClass;
|
||||
}
|
||||
|
||||
private static Class<?> entityClass(final Class<?> entityOrProxyClass) {
|
||||
return entityOrProxyClass.isAnnotationPresent(Entity.class)
|
||||
? entityOrProxyClass
|
||||
: entityOrProxyClass.getSuperclass() == null
|
||||
? null
|
||||
: entityClass(entityOrProxyClass.getSuperclass());
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package net.hostsharing.hsadminng.rbac.grant;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
|
||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -22,7 +22,7 @@ public class RbacGrantController implements RbacGrantsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private RbacGrantRepository rbacGrantRepository;
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.hostsharing.hsadminng.rbac.role;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi;
|
||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -18,7 +18,7 @@ public class RbacRoleController implements RbacRolesApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private RbacRoleRepository rbacRoleRepository;
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.hostsharing.hsadminng.rbac.subject;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
|
||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectPermissionResource;
|
||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource;
|
||||
@ -21,7 +21,7 @@ public class RbacSubjectController implements RbacSubjectsApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private RbacSubjectRepository rbacSubjectRepository;
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.hostsharing.hsadminng.rbac.test.cust;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
|
||||
import net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -21,7 +21,7 @@ public class TestCustomerController implements TestCustomersApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private TestCustomerRepository testCustomerRepository;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.hostsharing.hsadminng.rbac.test.pac;
|
||||
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestPackagesApi;
|
||||
@ -21,7 +21,7 @@ public class TestPackageController implements TestPackagesApi {
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private Mapper mapper;
|
||||
private StandardMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private TestPackageRepository testPackageRepository;
|
||||
|
@ -1,17 +1,20 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
@ -28,13 +31,14 @@ import java.util.UUID;
|
||||
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
||||
import static org.hamcrest.Matchers.matchesRegex;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(HsBookingItemController.class)
|
||||
@Import(Mapper.class)
|
||||
@Import({StrictMapper.class, JsonObjectMapperConfiguration.class})
|
||||
@RunWith(SpringRunner.class)
|
||||
class HsBookingItemControllerRestTest {
|
||||
|
||||
@ -44,8 +48,12 @@ class HsBookingItemControllerRestTest {
|
||||
@MockBean
|
||||
Context contextMock;
|
||||
|
||||
@Mock
|
||||
EntityManager em;
|
||||
@Autowired
|
||||
@SuppressWarnings("unused") // not used in test, but in controller class
|
||||
StrictMapper mapper;
|
||||
|
||||
@MockBean
|
||||
EntityManagerWrapper em;
|
||||
|
||||
@MockBean
|
||||
EntityManagerFactory emf;
|
||||
@ -56,6 +64,16 @@ class HsBookingItemControllerRestTest {
|
||||
@MockBean
|
||||
HsBookingItemRbacRepository rbacBookingItemRepo;
|
||||
|
||||
@TestConfiguration
|
||||
public static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public EntityManager entityManager() {
|
||||
return mock(EntityManager.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
when(emf.createEntityManager()).thenReturn(em);
|
||||
|
@ -35,7 +35,8 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.org")
|
||||
entry("domainName", "example.org"),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -55,6 +56,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.org"),
|
||||
entry("targetUnixUser", "xyz00"),
|
||||
entry("verificationCode", "1234-5678-9100")
|
||||
))
|
||||
.build();
|
||||
@ -73,7 +75,8 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253))
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -91,7 +94,8 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254))
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -102,6 +106,44 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.domainName' length is expected to be at max 253 but length of 'dfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.example.org' is 254");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsValidUnixUser() {
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.com"),
|
||||
entry("targetUnixUser", "xyz00-test")
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidUnixUser() {
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.com"),
|
||||
entry("targetUnixUser", "xyz00test")
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.targetUnixUser' = 'xyz00test' is not a valid unix-user name");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"de", "com", "net", "org", "actually-any-top-level-domain",
|
||||
@ -123,7 +165,8 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", secondLevelRegistrarDomain)
|
||||
entry("domainName", secondLevelRegistrarDomain),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -148,7 +191,8 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", secondLevelRegistrarDomain)
|
||||
entry("domainName", secondLevelRegistrarDomain),
|
||||
entry("targetUnixUser", "xyz00")
|
||||
))
|
||||
.build();
|
||||
|
||||
@ -170,6 +214,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
// then
|
||||
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
||||
"{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[[^.]+, (co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, maxLength=253, required=true, writeOnce=true}",
|
||||
"{type=string, propertyName=targetUnixUser, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}$|^[a-z][a-z0-9]{2}[0-9]{2}-[a-z0-9\\._-]+$], matchesRegExDescription=is not a valid unix-user name, maxLength=253, required=true, writeOnce=true}",
|
||||
"{type=string, propertyName=verificationCode, minLength=12, maxLength=64, computed=IN_INIT}");
|
||||
}
|
||||
}
|
||||
|