diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index a46b3d60..828ffe0f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -11,6 +11,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; @@ -25,15 +26,16 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.Transient; import jakarta.persistence.Version; import java.io.IOException; import java.time.LocalDate; +import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.mapper.PatchableMap.mapToString; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; @@ -65,7 +67,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { .withProp(e -> e.getDebitor().toShortString()) .withProp(e -> e.getValidity().asString()) .withProp(HsBookingItemEntity::getCaption) - .withProp(e -> mapToString(e.getResources())) + .withProp(HsBookingItemEntity::getResources) .quotedValues(false); @Id @@ -91,7 +93,10 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Setter(AccessLevel.NONE) @Type(JsonType.class) @Column(columnDefinition = "resources") - private Map resources = new TreeMap<>(); + private Map resources = new HashMap<>(); + + @Transient + private PatchableMapWrapper resourcesWrapper; public void setValidFrom(final LocalDate validFrom) { setValidity(toPostgresDateRange(validFrom, getValidTo())); @@ -109,6 +114,17 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { return upperInclusiveFromPostgresDateRange(getValidity()); } + public PatchableMapWrapper getResources() { + if ( resourcesWrapper == null ) { + resourcesWrapper = new PatchableMapWrapper(resources); + } + return resourcesWrapper; + } + + public void putResources(Map entries) { + resourcesWrapper.assign(entries); + } + @Override public String toString() { return stringify.apply(this); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java index 491fea11..39fb863d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcher.java @@ -25,19 +25,13 @@ public class HsBookingItemEntityPatcher implements EntityPatcher setResources(entity, r)); + .ifPresent(r -> entity.putResources(objectToMap(resource.getResources()))); OptionalFromJson.of(resource.getValidFrom()) .ifPresent(entity::setValidFrom); OptionalFromJson.of(resource.getValidTo()) .ifPresent(entity::setValidTo); } - private static void setResources( - final HsBookingItemEntity entity, - final ArbitraryBookingResourcesJsonResource arbitraryBookingResourcesJsonResource) { - entity.getResources().putAll(objectToMap(arbitraryBookingResourcesJsonResource)); - } - static Map objectToMap(final Object obj) { return stream(obj.getClass().getDeclaredFields()) .map(field -> { diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java new file mode 100644 index 00000000..abcc9c4f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchMap.java @@ -0,0 +1,30 @@ +package net.hostsharing.hsadminng.mapper; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import java.util.TreeMap; + +import static java.util.Arrays.stream; + +/** + * This is a map which can take key-value-pairs where the value can be null + * thus JSON nullable object structures from HTTP PATCH can be represented. + */ +public class PatchMap extends TreeMap { + + public PatchMap(final ImmutablePair[] entries) { + stream(entries).forEach(r -> put(r.getKey(), r.getValue())); + } + +// @SafeVarargs +// public static Map patchMap(final ImmutablePair... entries) { +// return new PatchMap(entries); +// } +// +// @NotNull +// public static ImmutablePair entry(final String key, final Object value) { +// return new ImmutablePair<>(key, value); +// } +} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java deleted file mode 100644 index 45669670..00000000 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMap.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.hostsharing.hsadminng.mapper; - -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import org.apache.commons.lang3.tuple.ImmutablePair; - -import jakarta.validation.constraints.NotNull; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiConsumer; - -import static java.util.stream.Collectors.joining; - -public class PatchableMap { - - @SafeVarargs - public static Map patchableMap(final ImmutablePair... entries) { - final var map = new HashMap(); - Arrays.stream(entries).forEach(r -> map.put(r.getKey(), r.getValue())); - return map; - } - - @NotNull - public static ImmutablePair entry(final String key, final Object value) { - return new ImmutablePair<>(key, value); - } - - public static BiConsumer> assignMap = (HsBookingItemEntity i, Map r) -> { - i.getResources().clear(); - i.getResources().putAll(r); - }; - - public static String mapToString(final Map resources) { - return "{ " - + ( - resources.keySet().stream().sorted() - .map(k -> k + ": " + resources.get(k))) - .collect(joining(", ") - ) - + " }"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java new file mode 100644 index 00000000..beb6cc5b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java @@ -0,0 +1,107 @@ +package net.hostsharing.hsadminng.mapper; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +import jakarta.validation.constraints.NotNull; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +/** This class wraps another (usually persistent) map and + * supports applying `PatchMap` as well as a toString method with stable entry order. + */ +public class PatchableMapWrapper implements Map { + + private final Map delegate; + + public PatchableMapWrapper(final Map map) { + delegate = map; + } + + @NotNull + public static ImmutablePair entry(final String key, final Object value) { + return new ImmutablePair<>(key, value); + } + + public void assign(final Map entries) { + delegate.clear(); + delegate.putAll(entries); + } + + public String toString() { + return "{ " + + ( + keySet().stream().sorted() + .map(k -> k + ": " + get(k))) + .collect(joining(", ") + ) + + " }"; + } + + // --- below just delegating methods -------------------------------- + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + return delegate.containsValue(value); + } + + @Override + public Object get(final Object key) { + return delegate.get(key); + } + + @Override + public Object put(final String key, final Object value) { + return delegate.put(key, value); + } + + @Override + public Object remove(final Object key) { + return delegate.remove(key); + } + + @Override + public void putAll(final Map m) { + delegate.putAll(m); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Collection values() { + return delegate.values(); + } + + @Override + public Set> entrySet() { + return delegate.entrySet(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index da88a87e..f5dafb11 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -233,8 +233,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validFrom": "2020-06-05", "validTo": "2022-12-31", "resources": { - "CPUs": "2", - "SSD-storage": "512" + "CPUs": "4", + "HDD_storage": null, + "SSD-storage": "4096" } } """) @@ -249,7 +250,10 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some test-booking", "validFrom": "2020-06-05", "validTo": "2022-12-31", - "resources": null + "resources": { + "CPUs": "2", + "SSD-storage": "2048" + } } """)); // @formatter:on @@ -290,7 +294,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "caption": "some test-booking", "validFrom": "2022-11-01", "validTo": "2022-12-31", - "resources": null + "resources": { + "CPUs": 5 + } } """)); // @formatter:on diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index afc825e0..5163305f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -4,7 +4,6 @@ import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.ArbitraryBookingResourcesJsonResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; -import net.hostsharing.hsadminng.mapper.PatchableMap; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -92,7 +91,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< "resources", HsBookingItemPatchResource::setResources, PATCHED_RESOURCES, - PatchableMap.assignMap, + HsBookingItemEntity::putResources, objectToMap(PATCHED_RESOURCES)) .notNullable(), new JsonNullableProperty<>( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index e90b0cb6..e1558f95 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -22,9 +22,9 @@ import jakarta.servlet.http.HttpServletRequest; import java.time.LocalDate; import java.util.Arrays; import java.util.List; +import java.util.Map; -import static net.hostsharing.hsadminng.mapper.PatchableMap.entry; -import static net.hostsharing.hsadminng.mapper.PatchableMap.patchableMap; +import static java.util.Map.entry; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; @@ -312,7 +312,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup .caption("some temp booking item") .validity(Range.closedOpen( LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01"))) - .resources(patchableMap( + .resources(Map.ofEntries( entry("CPUs", 1), entry("SSD-storage", 256))) .build();