This commit is contained in:
Michael Hoennig 2024-04-14 16:11:41 +02:00
parent 73d0ef8f78
commit 2ebbea1c24
8 changed files with 171 additions and 61 deletions

View File

@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; 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;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
@ -25,15 +26,16 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version; import jakarta.persistence.Version;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import static java.util.Optional.ofNullable; 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.lowerInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; 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.getDebitor().toShortString())
.withProp(e -> e.getValidity().asString()) .withProp(e -> e.getValidity().asString())
.withProp(HsBookingItemEntity::getCaption) .withProp(HsBookingItemEntity::getCaption)
.withProp(e -> mapToString(e.getResources())) .withProp(HsBookingItemEntity::getResources)
.quotedValues(false); .quotedValues(false);
@Id @Id
@ -91,7 +93,10 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@Type(JsonType.class) @Type(JsonType.class)
@Column(columnDefinition = "resources") @Column(columnDefinition = "resources")
private Map<String, Object> resources = new TreeMap<>(); private Map<String, Object> resources = new HashMap<>();
@Transient
private PatchableMapWrapper resourcesWrapper;
public void setValidFrom(final LocalDate validFrom) { public void setValidFrom(final LocalDate validFrom) {
setValidity(toPostgresDateRange(validFrom, getValidTo())); setValidity(toPostgresDateRange(validFrom, getValidTo()));
@ -109,6 +114,17 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
return upperInclusiveFromPostgresDateRange(getValidity()); return upperInclusiveFromPostgresDateRange(getValidity());
} }
public PatchableMapWrapper getResources() {
if ( resourcesWrapper == null ) {
resourcesWrapper = new PatchableMapWrapper(resources);
}
return resourcesWrapper;
}
public void putResources(Map<String, Object> entries) {
resourcesWrapper.assign(entries);
}
@Override @Override
public String toString() { public String toString() {
return stringify.apply(this); return stringify.apply(this);

View File

@ -25,19 +25,13 @@ public class HsBookingItemEntityPatcher implements EntityPatcher<HsBookingItemPa
OptionalFromJson.of(resource.getCaption()) OptionalFromJson.of(resource.getCaption())
.ifPresent(entity::setCaption); .ifPresent(entity::setCaption);
Optional.ofNullable(resource.getResources()) Optional.ofNullable(resource.getResources())
.ifPresent(r -> setResources(entity, r)); .ifPresent(r -> entity.putResources(objectToMap(resource.getResources())));
OptionalFromJson.of(resource.getValidFrom()) OptionalFromJson.of(resource.getValidFrom())
.ifPresent(entity::setValidFrom); .ifPresent(entity::setValidFrom);
OptionalFromJson.of(resource.getValidTo()) OptionalFromJson.of(resource.getValidTo())
.ifPresent(entity::setValidTo); .ifPresent(entity::setValidTo);
} }
private static void setResources(
final HsBookingItemEntity entity,
final ArbitraryBookingResourcesJsonResource arbitraryBookingResourcesJsonResource) {
entity.getResources().putAll(objectToMap(arbitraryBookingResourcesJsonResource));
}
static Map<String, Object> objectToMap(final Object obj) { static Map<String, Object> objectToMap(final Object obj) {
return stream(obj.getClass().getDeclaredFields()) return stream(obj.getClass().getDeclaredFields())
.map(field -> { .map(field -> {

View File

@ -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<String, Object> {
public PatchMap(final ImmutablePair<String, Object>[] entries) {
stream(entries).forEach(r -> put(r.getKey(), r.getValue()));
}
// @SafeVarargs
// public static Map<String, Object> patchMap(final ImmutablePair<String, Object>... entries) {
// return new PatchMap(entries);
// }
//
// @NotNull
// public static ImmutablePair<String, Object> entry(final String key, final Object value) {
// return new ImmutablePair<>(key, value);
// }
}

View File

@ -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<String, Object> patchableMap(final ImmutablePair<String, Object>... entries) {
final var map = new HashMap<String, Object>();
Arrays.stream(entries).forEach(r -> map.put(r.getKey(), r.getValue()));
return map;
}
@NotNull
public static ImmutablePair<String, Object> entry(final String key, final Object value) {
return new ImmutablePair<>(key, value);
}
public static BiConsumer<? extends Object, ? super Map<String, Object>> assignMap = (HsBookingItemEntity i, Map<String, Object> r) -> {
i.getResources().clear();
i.getResources().putAll(r);
};
public static String mapToString(final Map<String, Object> resources) {
return "{ "
+ (
resources.keySet().stream().sorted()
.map(k -> k + ": " + resources.get(k)))
.collect(joining(", ")
)
+ " }";
}
}

View File

@ -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<String, Object> {
private final Map<String, Object> delegate;
public PatchableMapWrapper(final Map<String, Object> map) {
delegate = map;
}
@NotNull
public static ImmutablePair<String, Object> entry(final String key, final Object value) {
return new ImmutablePair<>(key, value);
}
public void assign(final Map<String, Object> 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<? extends String, ?> m) {
delegate.putAll(m);
}
@Override
public void clear() {
delegate.clear();
}
@Override
public Set<String> keySet() {
return delegate.keySet();
}
@Override
public Collection<Object> values() {
return delegate.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
return delegate.entrySet();
}
}

View File

@ -233,8 +233,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"validFrom": "2020-06-05", "validFrom": "2020-06-05",
"validTo": "2022-12-31", "validTo": "2022-12-31",
"resources": { "resources": {
"CPUs": "2", "CPUs": "4",
"SSD-storage": "512" "HDD_storage": null,
"SSD-storage": "4096"
} }
} }
""") """)
@ -249,7 +250,10 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"caption": "some test-booking", "caption": "some test-booking",
"validFrom": "2020-06-05", "validFrom": "2020-06-05",
"validTo": "2022-12-31", "validTo": "2022-12-31",
"resources": null "resources": {
"CPUs": "2",
"SSD-storage": "2048"
}
} }
""")); // @formatter:on """)); // @formatter:on
@ -290,7 +294,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
"caption": "some test-booking", "caption": "some test-booking",
"validFrom": "2022-11-01", "validFrom": "2022-11-01",
"validTo": "2022-12-31", "validTo": "2022-12-31",
"resources": null "resources": {
"CPUs": 5
}
} }
""")); // @formatter:on """)); // @formatter:on

View File

@ -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.ArbitraryBookingResourcesJsonResource;
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.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.mapper.PatchableMap;
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
@ -92,7 +91,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase<
"resources", "resources",
HsBookingItemPatchResource::setResources, HsBookingItemPatchResource::setResources,
PATCHED_RESOURCES, PATCHED_RESOURCES,
PatchableMap.assignMap, HsBookingItemEntity::putResources,
objectToMap(PATCHED_RESOURCES)) objectToMap(PATCHED_RESOURCES))
.notNullable(), .notNullable(),
new JsonNullableProperty<>( new JsonNullableProperty<>(

View File

@ -22,9 +22,9 @@ import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import static net.hostsharing.hsadminng.mapper.PatchableMap.entry; import static java.util.Map.entry;
import static net.hostsharing.hsadminng.mapper.PatchableMap.patchableMap;
import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted;
@ -312,7 +312,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
.caption("some temp booking item") .caption("some temp booking item")
.validity(Range.closedOpen( .validity(Range.closedOpen(
LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01"))) LocalDate.parse("2020-01-01"), LocalDate.parse("2023-01-01")))
.resources(patchableMap( .resources(Map.ofEntries(
entry("CPUs", 1), entry("CPUs", 1),
entry("SSD-storage", 256))) entry("SSD-storage", 256)))
.build(); .build();