introduce PatchableMap and make HsBookingItemEntity.resources readonly

This commit is contained in:
Michael Hoennig 2024-04-14 13:07:24 +02:00
parent 0876b2ce40
commit fb35c46e8f
4 changed files with 82 additions and 38 deletions

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
import io.hypersistence.utils.hibernate.type.range.Range;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@ -10,31 +11,44 @@ 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.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.function.BinaryOperator;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.joining;
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;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@ -51,18 +65,9 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
.withProp(e -> e.getDebitor().toShortString())
.withProp(e -> e.getValidity().asString())
.withProp(HsBookingItemEntity::getCaption)
.withProp(HsBookingItemEntity::getResourcesAsString)
.withProp(e -> mapToString(e.getResources()))
.quotedValues(false);
private String getResourcesAsString() {
return "{ " +
(
resources.keySet().stream().sorted()
.map(k -> k + ": " + resources.get(k)))
.collect(joining(", ")
) + " }";
}
@Id
@GeneratedValue
private UUID uuid;
@ -83,6 +88,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
private String caption;
@Builder.Default
@Setter(AccessLevel.NONE)
@Type(JsonType.class)
@Column(columnDefinition = "resources")
private Map<String, Object> resources = new TreeMap<>();
@ -103,7 +109,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
return upperInclusiveFromPostgresDateRange(getValidity());
}
@Override
public String toString() {
return stringify.apply(this);
@ -115,17 +120,13 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
":" + caption;
}
private static BinaryOperator<Object> thereIsOnlyOneValuePerKey(Object o, Object o1) {
return (a, b) -> a;
}
public static RbacView rbac() {
return rbacViewFor("bookingItem", HsBookingItemEntity.class)
.withIdentityView(SQL.query("""
SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName
FROM hs_booking_item i
JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid
"""))
SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName
FROM hs_booking_item i
JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid
"""))
.withRestrictedViewOrderBy(SQL.expression("validity"))
.withUpdatableColumns("version", "validity", "resources")

View File

@ -0,0 +1,42 @@
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

@ -4,6 +4,7 @@ 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;
@ -63,7 +64,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase<
final var entity = new HsBookingItemEntity();
entity.setUuid(INITIAL_BOOKING_ITEM_UUID);
entity.setDebitor(TEST_DEBITOR);
entity.setResources(objectToMap(INITIAL_RESOURCES));
entity.getResources().putAll(objectToMap(INITIAL_RESOURCES));
entity.setCaption(INITIAL_CAPTION);
entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM));
return entity;
@ -91,7 +92,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase<
"resources",
HsBookingItemPatchResource::setResources,
PATCHED_RESOURCES,
HsBookingItemEntity::setResources,
PatchableMap.assignMap,
objectToMap(PATCHED_RESOURCES))
.notNullable(),
new JsonNullableProperty<>(

View File

@ -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 java.util.Map.entry;
import static net.hostsharing.hsadminng.mapper.PatchableMap.entry;
import static net.hostsharing.hsadminng.mapper.PatchableMap.patchableMap;
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;
@ -186,18 +186,18 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
@Test
public void hostsharingAdmin_canUpdateArbitraryBookingItem() {
// given
final var givenBookingItem = givenSomeTemporaryBookingItem(1000111);
final var givenBookingItemUuid = givenSomeTemporaryBookingItem(1000111).getUuid();
// when
final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
givenBookingItem.setResources(Map.ofEntries(
entry("CPUs", 2),
entry("SSD-storage", 512),
entry("HDD-storage", 2048)));
givenBookingItem.setValidity(Range.closedOpen(
final var foundBookingItem = em.find(HsBookingItemEntity.class, givenBookingItemUuid);
foundBookingItem.getResources().put("CPUs", 2);
foundBookingItem.getResources().remove("SSD-storage");
foundBookingItem.getResources().put("HSD-storage", 2048);
foundBookingItem.setValidity(Range.closedOpen(
LocalDate.parse("2019-05-17"), LocalDate.parse("2023-01-01")));
return toCleanup(bookingItemRepo.save(givenBookingItem));
return toCleanup(bookingItemRepo.save(foundBookingItem));
});
// then
@ -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(Map.ofEntries(
.resources(patchableMap(
entry("CPUs", 1),
entry("SSD-storage", 256)))
.build();