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:
Michael Hoennig 2024-09-26 10:51:27 +02:00
parent d949604d70
commit cb4aecb9c8
47 changed files with 474 additions and 139 deletions

View File

@ -550,12 +550,37 @@ Dependency versions can be automatically upgraded to the latest available versio
gw useLatestVersions 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. 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). 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 ...
### How to Configure .pgpass for the Default PostgreSQL Database? ### How to Configure .pgpass for the Default PostgreSQL Database?

View File

@ -9,15 +9,25 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface DisplayAs { public @interface DisplayAs {
class DisplayName { class DisplayName {
public static String of(final Class<?> clazz) { 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(); return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName();
} }
public static String of(@NotNull final Object instance) { public static String of(@NotNull final Object instance) {
return of(instance.getClass()); 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 ""; String value() default "";

View File

@ -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.HsBookingItemInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; 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.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.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
import net.hostsharing.hsadminng.mapper.KeyValueMap; 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.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -30,13 +31,13 @@ public class HsBookingItemController implements HsBookingItemsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsBookingItemRbacRepository bookingItemRepo; private HsBookingItemRbacRepository bookingItemRepo;
@PersistenceContext @Autowired
private EntityManager em; private EntityManagerWrapper em;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -48,7 +49,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid); 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); return ResponseEntity.ok(resources);
} }
@ -62,15 +63,20 @@ public class HsBookingItemController implements HsBookingItemsApi {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var mapped = new BookingItemEntitySaveProcessor(em, entityToSave)
final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave)); .preprocessEntity()
.validateEntity()
.prepareForSave()
.save()
.validateContext()
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
.revampProperties();
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/booking/items/{id}") .path("/api/hs/booking/items/{id}")
.buildAndExpand(saved.getUuid()) .buildAndExpand(mapped.getUuid())
.toUri(); .toUri();
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped); 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 result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
return result return result
.map(bookingItemEntity -> ResponseEntity.ok( .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()); .orElseGet(() -> ResponseEntity.notFound().build());
} }
@ -120,18 +126,21 @@ public class HsBookingItemController implements HsBookingItemsApi {
new HsBookingItemEntityPatcher(current).apply(body); new HsBookingItemEntityPatcher(current).apply(body);
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current)); 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); 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()); resource.setValidFrom(entity.getValidity().lower());
if (entity.getValidity().hasUpperBound()) { if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1)); 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) -> { 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.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
entity.putResources(KeyValueMap.from(resource.getResources())); entity.putResources(KeyValueMap.from(resource.getResources()));
}; };

View File

@ -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;
}
}

View File

@ -48,10 +48,11 @@ public class HsBookingItemEntityValidatorRegistry {
} }
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) { public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType());
return HsEntityValidator.doWithEntityManager(em, () -> return HsEntityValidator.doWithEntityManager(em, () ->
HsEntityValidator.sequentiallyValidate( HsEntityValidator.sequentiallyValidate(
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem), () -> bookingItemValidator.validateEntity(bookingItem),
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) () -> bookingItemValidator.validateContext(bookingItem))
); );
} }

View File

@ -1,3 +1,4 @@
package net.hostsharing.hsadminng.hs.booking.item.validators; package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; 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 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 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"; public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
HsDomainSetupBookingItemValidator() { HsDomainSetupBookingItemValidator() {
@ -24,6 +28,12 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name") .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") .notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
.required(), .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) stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
.minLength(12) .minLength(12)
.maxLength(64) .maxLength(64)

View File

@ -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.HsBookingProjectInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource; 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.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.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -25,7 +25,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsBookingProjectRbacRepository bookingProjectRepo; private HsBookingProjectRbacRepository bookingProjectRepo;

View File

@ -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.HsHostingAssetResource;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import net.hostsharing.hsadminng.mapper.KeyValueMap; 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 net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -35,7 +35,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsHostingAssetRbacRepository rbacAssetRepo; private HsHostingAssetRbacRepository rbacAssetRepo;

View File

@ -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.api.HsOfficeBankAccountsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource; 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.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.BicUtil;
import org.iban4j.IbanUtil; import org.iban4j.IbanUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -24,7 +24,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeBankAccountRepository bankAccountRepo; private HsOfficeBankAccountRepository bankAccountRepo;

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.contact; 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.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
@ -26,7 +26,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeContactRbacRepository contactRepo; private HsOfficeContactRbacRepository contactRepo;

View File

@ -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.api.HsOfficeCoopAssetsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.errors.MultiValidationException; 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.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -29,7 +29,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;

View File

@ -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.HsOfficeCoopSharesTransactionInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
import net.hostsharing.hsadminng.errors.MultiValidationException; 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.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -31,7 +31,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;

View File

@ -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.generated.api.v1.model.HsOfficeDebitorResource;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.rbac.object.BaseEntity; import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -23,7 +23,6 @@ import jakarta.validation.ValidationException;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
@RestController @RestController
@ -34,7 +33,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeDebitorRepository debitorRepo; private HsOfficeDebitorRepository debitorRepo;
@ -42,6 +41,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Autowired @Autowired
private HsOfficeRelationRealRepository relrealRepo; private HsOfficeRelationRealRepository relrealRepo;
@Autowired
private EntityExistsValidator entityValidator;
@PersistenceContext @PersistenceContext
private EntityManager em; private EntityManager em;
@ -84,10 +86,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class); final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
if ( body.getDebitorRel() != null ) { if ( body.getDebitorRel() != null ) {
body.getDebitorRel().setType(DEBITOR.name()); body.getDebitorRel().setType(DEBITOR.name());
final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationRealEntity.class); final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor()); entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder()); entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
validateEntityExists("debitorRel.contactUuid", debitorRel.getContact()); entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
entityToSave.setDebitorRel(relrealRepo.save(debitorRel)); entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
} else { } else {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid()); final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
@ -160,15 +162,4 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class); final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
return ResponseEntity.ok(mapped); 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;
}
} }

View File

@ -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.HsOfficeMembershipInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; 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.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.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -24,7 +24,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeMembershipRepository membershipRepo; private HsOfficeMembershipRepository membershipRepo;

View File

@ -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.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher; 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 net.hostsharing.hsadminng.mapper.OptionalFromJson;
import java.util.Optional; import java.util.Optional;
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> { public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
private final Mapper mapper; private final StandardMapper mapper;
private final HsOfficeMembershipEntity entity; private final HsOfficeMembershipEntity entity;
public HsOfficeMembershipEntityPatcher( public HsOfficeMembershipEntityPatcher(
final Mapper mapper, final StandardMapper mapper,
final HsOfficeMembershipEntity entity) { final HsOfficeMembershipEntity entity) {
this.mapper = mapper; this.mapper = mapper;
this.entity = entity; this.entity = entity;

View File

@ -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.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; 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 net.hostsharing.hsadminng.rbac.object.BaseEntity;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -36,7 +36,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficePartnerRepository partnerRepo; private HsOfficePartnerRepository partnerRepo;

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.person; 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.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
@ -23,7 +23,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficePersonRepository personRepo; private HsOfficePersonRepository personRepo;

View File

@ -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.api.HsOfficeRelationsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; 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.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -28,7 +28,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeRelationRbacRepository relationRbacRepo; private HsOfficeRelationRbacRepository relationRbacRepo;

View File

@ -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.HsOfficeSepaMandateInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource; 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.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.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -28,7 +28,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeSepaMandateRepository sepaMandateRepo; private HsOfficeSepaMandateRepository sepaMandateRepo;

View File

@ -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) { 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 -> { stream(propertyValidators).forEach(p -> {
if (p.isWriteOnly()) { if (p.isWriteOnly()) {
copy.remove(p.propertyName); copy.remove(p.propertyName);

View File

@ -11,18 +11,20 @@ import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName; import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
/** /**
* A nicer API for ModelMapper. * A nicer API for ModelMapper.
*/ */
public class Mapper extends ModelMapper { abstract class Mapper extends ModelMapper {
@PersistenceContext @PersistenceContext
EntityManager em; EntityManager em;
public Mapper() { Mapper() {
getConfiguration().setAmbiguityIgnored(true); getConfiguration().setAmbiguityIgnored(true);
} }
@ -45,8 +47,12 @@ public class Mapper extends ModelMapper {
@Override @Override
public <D> D map(final Object source, final Class<D> destinationType) { 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); final var target = super.map(source, destinationType);
for (Field f : destinationType.getDeclaredFields()) { for (Field f : getDeclaredFieldsIncludingSuperClasses(destinationType)) {
if (f.getAnnotation(ManyToOne.class) == null) { if (f.getAnnotation(ManyToOne.class) == null) {
continue; continue;
} }
@ -64,18 +70,30 @@ public class Mapper extends ModelMapper {
if (subEntityUuid == null) { if (subEntityUuid == null) {
continue; continue;
} }
ReflectionUtils.setField(f, target, findEntityById(f.getType(), subEntityUuid)); ReflectionUtils.setField(f, target, fetchEntity(namePrefix + f.getName() + ".uuid", f.getType(), subEntityUuid));
} }
return target; return target;
} }
private Object findEntityById(final Class<?> entityClass, final Object subEntityUuid) { private static <D> Field[] getDeclaredFieldsIncludingSuperClasses(final Class<D> destinationType) {
// using getReference would be more efficent, but results in very technical error messages if (destinationType == null) {
final var entity = em.find(entityClass, subEntityUuid); 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) { if (entity != null) {
return entity; 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) { 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); postMapper.accept(source, target);
return 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;
}
} }

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.rbac.grant; package net.hostsharing.hsadminng.rbac.grant;
import net.hostsharing.hsadminng.context.Context;