introduce PatchableMap and make HsBookingItemEntity.resources readonly

This commit is contained in:
Michael Hoennig 2024-04-14 13:07:24 +02:00
parent 89ef2d9126
commit 73d0ef8f78
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.json.JsonType;
import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType; import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
import io.hypersistence.utils.hibernate.type.range.Range; import io.hypersistence.utils.hibernate.type.range.Range;
import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
@ -10,31 +11,44 @@ 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.rbac.rbacobject.RbacObject;
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.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type; 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.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
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 java.util.function.BinaryOperator;
import static java.util.Optional.ofNullable; 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.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;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; 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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*; 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.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; 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.getDebitor().toShortString())
.withProp(e -> e.getValidity().asString()) .withProp(e -> e.getValidity().asString())
.withProp(HsBookingItemEntity::getCaption) .withProp(HsBookingItemEntity::getCaption)
.withProp(HsBookingItemEntity::getResourcesAsString) .withProp(e -> mapToString(e.getResources()))
.quotedValues(false); .quotedValues(false);
private String getResourcesAsString() {
return "{ " +
(
resources.keySet().stream().sorted()
.map(k -> k + ": " + resources.get(k)))
.collect(joining(", ")
) + " }";
}
@Id @Id
@GeneratedValue @GeneratedValue
private UUID uuid; private UUID uuid;
@ -83,6 +88,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
private String caption; private String caption;
@Builder.Default @Builder.Default
@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 TreeMap<>();
@ -103,7 +109,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
return upperInclusiveFromPostgresDateRange(getValidity()); return upperInclusiveFromPostgresDateRange(getValidity());
} }
@Override @Override
public String toString() { public String toString() {
return stringify.apply(this); return stringify.apply(this);
@ -115,17 +120,13 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
":" + caption; ":" + caption;
} }
private static BinaryOperator<Object> thereIsOnlyOneValuePerKey(Object o, Object o1) {
return (a, b) -> a;
}
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("bookingItem", HsBookingItemEntity.class) return rbacViewFor("bookingItem", HsBookingItemEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName SELECT i.uuid as uuid, d.idName || ':' || i.caption as idName
FROM hs_booking_item i FROM hs_booking_item i
JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid JOIN hs_office_debitor_iv d ON d.uuid = i.debitorUuid
""")) """))
.withRestrictedViewOrderBy(SQL.expression("validity")) .withRestrictedViewOrderBy(SQL.expression("validity"))
.withUpdatableColumns("version", "validity", "resources") .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.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;
@ -63,7 +64,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase<
final var entity = new HsBookingItemEntity(); final var entity = new HsBookingItemEntity();
entity.setUuid(INITIAL_BOOKING_ITEM_UUID); entity.setUuid(INITIAL_BOOKING_ITEM_UUID);
entity.setDebitor(TEST_DEBITOR); entity.setDebitor(TEST_DEBITOR);
entity.setResources(objectToMap(INITIAL_RESOURCES)); entity.getResources().putAll(objectToMap(INITIAL_RESOURCES));
entity.setCaption(INITIAL_CAPTION); entity.setCaption(INITIAL_CAPTION);
entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM));
return entity; return entity;
@ -91,7 +92,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase<
"resources", "resources",
HsBookingItemPatchResource::setResources, HsBookingItemPatchResource::setResources,
PATCHED_RESOURCES, PATCHED_RESOURCES,
HsBookingItemEntity::setResources, PatchableMap.assignMap,
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 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.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;
@ -186,18 +186,18 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
@Test @Test
public void hostsharingAdmin_canUpdateArbitraryBookingItem() { public void hostsharingAdmin_canUpdateArbitraryBookingItem() {
// given // given
final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); final var givenBookingItemUuid = givenSomeTemporaryBookingItem(1000111).getUuid();
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
givenBookingItem.setResources(Map.ofEntries( final var foundBookingItem = em.find(HsBookingItemEntity.class, givenBookingItemUuid);
entry("CPUs", 2), foundBookingItem.getResources().put("CPUs", 2);
entry("SSD-storage", 512), foundBookingItem.getResources().remove("SSD-storage");
entry("HDD-storage", 2048))); foundBookingItem.getResources().put("HSD-storage", 2048);
givenBookingItem.setValidity(Range.closedOpen( foundBookingItem.setValidity(Range.closedOpen(
LocalDate.parse("2019-05-17"), LocalDate.parse("2023-01-01"))); LocalDate.parse("2019-05-17"), LocalDate.parse("2023-01-01")));
return toCleanup(bookingItemRepo.save(givenBookingItem)); return toCleanup(bookingItemRepo.save(foundBookingItem));
}); });
// then // then
@ -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(Map.ofEntries( .resources(patchableMap(
entry("CPUs", 1), entry("CPUs", 1),
entry("SSD-storage", 256))) entry("SSD-storage", 256)))
.build(); .build();