office-related spec-clarifications and -amendmends (contact.emailaddresses+.phonenumbers JSON) (#50)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/50 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@ -98,7 +98,15 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
|
||||
private Map<String, Object> resources = new HashMap<>();
|
||||
|
||||
@Transient
|
||||
private PatchableMapWrapper resourcesWrapper;
|
||||
private PatchableMapWrapper<Object> resourcesWrapper;
|
||||
|
||||
public PatchableMapWrapper<Object> getResources() {
|
||||
return PatchableMapWrapper.of(resourcesWrapper, (newWrapper) -> {resourcesWrapper = newWrapper; }, resources );
|
||||
}
|
||||
|
||||
public void putResources(Map<String, Object> newResources) {
|
||||
getResources().assign(newResources);
|
||||
}
|
||||
|
||||
public void setValidFrom(final LocalDate validFrom) {
|
||||
setValidity(toPostgresDateRange(validFrom, getValidTo()));
|
||||
@ -116,20 +124,6 @@ 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<String, Object> entries) {
|
||||
if ( resourcesWrapper == null ) {
|
||||
resourcesWrapper = new PatchableMapWrapper(resources);
|
||||
}
|
||||
resourcesWrapper.assign(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
|
@ -105,20 +105,14 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
|
||||
private Map<String, Object> config = new HashMap<>();
|
||||
|
||||
@Transient
|
||||
private PatchableMapWrapper configWrapper;
|
||||
private PatchableMapWrapper<Object> configWrapper;
|
||||
|
||||
public PatchableMapWrapper getConfig() {
|
||||
if ( configWrapper == null ) {
|
||||
configWrapper = new PatchableMapWrapper(config);
|
||||
}
|
||||
return configWrapper;
|
||||
public PatchableMapWrapper<Object> getConfig() {
|
||||
return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config );
|
||||
}
|
||||
|
||||
public void putConfig(Map<String, Object> entries) {
|
||||
if ( configWrapper == null ) {
|
||||
configWrapper = new PatchableMapWrapper(config);
|
||||
}
|
||||
configWrapper.assign(entries);
|
||||
public void putConfig(Map<String, Object> newConfg) {
|
||||
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -14,6 +14,9 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
|
||||
|
||||
@RestController
|
||||
|
||||
@ -51,7 +54,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficeContactEntity.class);
|
||||
final var entityToSave = mapper.map(body, HsOfficeContactEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
|
||||
final var saved = contactRepo.save(entityToSave);
|
||||
|
||||
@ -108,10 +111,16 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
||||
|
||||
final var current = contactRepo.findByUuid(contactUuid).orElseThrow();
|
||||
|
||||
new HsOfficeContactEntityPatch(current).apply(body);
|
||||
new HsOfficeContactEntityPatcher(current).apply(body);
|
||||
|
||||
final var saved = contactRepo.save(current);
|
||||
final var mapped = mapper.map(saved, HsOfficeContactResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
entity.putEmailAddresses(from(resource.getEmailAddresses()));
|
||||
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
|
||||
};
|
||||
}
|
||||
|
@ -1,17 +1,22 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import io.hypersistence.utils.hibernate.type.json.JsonType;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||
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.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
@ -44,17 +49,45 @@ public class HsOfficeContactEntity implements Stringifyable, RbacObject {
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(name = "label")
|
||||
@Column(name = "label") // TODO.impl: rename to caption
|
||||
private String label;
|
||||
|
||||
@Column(name = "postaladdress")
|
||||
private String postalAddress; // TODO.spec: check if we really want multiple, if so: JSON-Array or Postgres-Array?
|
||||
private String postalAddress; // multiline free-format text
|
||||
|
||||
@Column(name = "emailaddresses", columnDefinition = "json")
|
||||
private String emailAddresses; // TODO.spec: check if we can really add multiple. format: ["eins@...", "zwei@..."]
|
||||
@Builder.Default
|
||||
@Setter(AccessLevel.NONE)
|
||||
@Type(JsonType.class)
|
||||
@Column(name = "emailaddresses")
|
||||
private Map<String, String> emailAddresses = new HashMap<>();
|
||||
|
||||
@Column(name = "phonenumbers", columnDefinition = "json")
|
||||
private String phoneNumbers; // TODO.spec: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" }
|
||||
@Transient
|
||||
private PatchableMapWrapper<String> emailAddressesWrapper;
|
||||
|
||||
@Builder.Default
|
||||
@Setter(AccessLevel.NONE)
|
||||
@Type(JsonType.class)
|
||||
@Column(name = "phonenumbers")
|
||||
private Map<String, String> phoneNumbers = new HashMap<>();
|
||||
|
||||
@Transient
|
||||
private PatchableMapWrapper<String> phoneNumbersWrapper;
|
||||
|
||||
public PatchableMapWrapper<String> getEmailAddresses() {
|
||||
return PatchableMapWrapper.of(emailAddressesWrapper, (newWrapper) -> {emailAddressesWrapper = newWrapper; }, emailAddresses );
|
||||
}
|
||||
|
||||
public void putEmailAddresses(Map<String, String> newEmailAddresses) {
|
||||
getEmailAddresses().assign(newEmailAddresses);
|
||||
}
|
||||
|
||||
public PatchableMapWrapper<String> getPhoneNumbers() {
|
||||
return PatchableMapWrapper.of(phoneNumbersWrapper, (newWrapper) -> {phoneNumbersWrapper = newWrapper; }, phoneNumbers );
|
||||
}
|
||||
|
||||
public void putPhoneNumbers(Map<String, String> newPhoneNumbers) {
|
||||
getPhoneNumbers().assign(newPhoneNumbers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@ -1,14 +1,17 @@
|
||||
package net.hostsharing.hsadminng.hs.office.contact;
|
||||
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactPatchResource;
|
||||
|
||||
class HsOfficeContactEntityPatch implements EntityPatcher<HsOfficeContactPatchResource> {
|
||||
import java.util.Optional;
|
||||
|
||||
class HsOfficeContactEntityPatcher implements EntityPatcher<HsOfficeContactPatchResource> {
|
||||
|
||||
private final HsOfficeContactEntity entity;
|
||||
|
||||
HsOfficeContactEntityPatch(final HsOfficeContactEntity entity) {
|
||||
HsOfficeContactEntityPatcher(final HsOfficeContactEntity entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@ -16,7 +19,9 @@ class HsOfficeContactEntityPatch implements EntityPatcher<HsOfficeContactPatchRe
|
||||
public void apply(final HsOfficeContactPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getLabel()).ifPresent(entity::setLabel);
|
||||
OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress);
|
||||
OptionalFromJson.of(resource.getEmailAddresses()).ifPresent(entity::setEmailAddresses);
|
||||
OptionalFromJson.of(resource.getPhoneNumbers()).ifPresent(entity::setPhoneNumbers);
|
||||
Optional.ofNullable(resource.getEmailAddresses())
|
||||
.ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses())));
|
||||
Optional.ofNullable(resource.getPhoneNumbers())
|
||||
.ifPresent(r -> entity.getPhoneNumbers().patch(KeyValueMap.from(resource.getPhoneNumbers())));
|
||||
}
|
||||
}
|
@ -170,7 +170,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
|
||||
"vatCountryCode",
|
||||
"vatBusiness",
|
||||
"vatReverseCharge",
|
||||
"defaultPrefix" /* TODO.spec: do we want that updatable? */)
|
||||
"defaultPrefix")
|
||||
.toRole("global", ADMIN).grantPermission(INSERT)
|
||||
|
||||
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR),
|
||||
|
@ -142,7 +142,7 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(AGENT, (with) -> {
|
||||
// TODO.spec: we need relation:PROXY, to allow changing the relation contact.
|
||||
// TODO.rbac: we need relation:PROXY, to allow changing the relation contact.
|
||||
// the alternative would be to move this to the relation:ADMIN role,
|
||||
// but then the partner holder person could update the partner relation itself,
|
||||
// see partner entity.
|
||||
|
@ -5,9 +5,9 @@ import java.util.Map;
|
||||
public class KeyValueMap {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> from(final Object obj) {
|
||||
if (obj instanceof Map<?, ?>) {
|
||||
return (Map<String, Object>) obj;
|
||||
public static <T> Map<String, T> from(final Object obj) {
|
||||
if (obj == null || obj instanceof Map<?, ?>) {
|
||||
return (Map<String, T>) obj;
|
||||
}
|
||||
throw new ClassCastException("Map expected, but got: " + obj);
|
||||
}
|
||||
|
@ -12,19 +12,19 @@ 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 class PatchMap<T> extends TreeMap<String, T> {
|
||||
|
||||
public PatchMap(final ImmutablePair<String, Object>[] entries) {
|
||||
public PatchMap(final ImmutablePair<String, T>[] entries) {
|
||||
stream(entries).forEach(r -> put(r.getKey(), r.getValue()));
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static Map<String, Object> patchMap(final ImmutablePair<String, Object>... entries) {
|
||||
public static <T> Map<String, T> patchMap(final ImmutablePair<String, Object>... entries) {
|
||||
return new PatchMap(entries);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ImmutablePair<String, Object> entry(final String key, final Object value) {
|
||||
public static <T> ImmutablePair<String, T> entry(final String key, final T value) {
|
||||
return new ImmutablePair<>(key, value);
|
||||
}
|
||||
}
|
||||
|
@ -6,31 +6,43 @@ import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
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> {
|
||||
public class PatchableMapWrapper<T> implements Map<String, T> {
|
||||
|
||||
private final Map<String, Object> delegate;
|
||||
private final Map<String, T> delegate;
|
||||
|
||||
public PatchableMapWrapper(final Map<String, Object> map) {
|
||||
private PatchableMapWrapper(final Map<String, T> map) {
|
||||
delegate = map;
|
||||
}
|
||||
|
||||
public static <T> PatchableMapWrapper<T> of(final PatchableMapWrapper<T> currentWrapper, final Consumer<PatchableMapWrapper<T>> setWrapper, final Map<String, T> target) {
|
||||
return ofNullable(currentWrapper).orElseGet(() -> {
|
||||
final var newWrapper = new PatchableMapWrapper<T>(target);
|
||||
setWrapper.accept(newWrapper);
|
||||
return newWrapper;
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ImmutablePair<String, Object> entry(final String key, final Object value) {
|
||||
public static <E> ImmutablePair<String, E> entry(final String key, final E value) {
|
||||
return new ImmutablePair<>(key, value);
|
||||
}
|
||||
|
||||
public void assign(final Map<String, Object> entries) {
|
||||
delegate.clear();
|
||||
delegate.putAll(entries);
|
||||
public void assign(final Map<String, T> entries) {
|
||||
if (entries != null ) {
|
||||
delegate.clear();
|
||||
delegate.putAll(entries);
|
||||
}
|
||||
}
|
||||
|
||||
public void patch(final Map<String, Object> patch) {
|
||||
public void patch(final Map<String, T> patch) {
|
||||
patch.forEach((key, value) -> {
|
||||
if (value == null) {
|
||||
remove(key);
|
||||
@ -73,22 +85,22 @@ public class PatchableMapWrapper implements Map<String, Object> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(final Object key) {
|
||||
public T get(final Object key) {
|
||||
return delegate.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object put(final String key, final Object value) {
|
||||
public T put(final String key, final T value) {
|
||||
return delegate.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object remove(final Object key) {
|
||||
public T remove(final Object key) {
|
||||
return delegate.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(final Map<? extends String, ?> m) {
|
||||
public void putAll(final @NotNull Map<? extends String, ? extends T> m) {
|
||||
delegate.putAll(m);
|
||||
}
|
||||
|
||||
@ -103,12 +115,12 @@ public class PatchableMapWrapper implements Map<String, Object> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Object> values() {
|
||||
public Collection<T> values() {
|
||||
return delegate.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
public Set<Entry<String, T>> entrySet() {
|
||||
return delegate.entrySet();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
@ -62,6 +64,7 @@ public final class Stringify<B> {
|
||||
final var propValues = props.stream()
|
||||
.map(prop -> PropertyValue.of(prop, prop.getter.apply(object)))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(PropertyValue::nonEmpty)
|
||||
.map(propVal -> {
|
||||
if (propVal.rawValue instanceof Stringifyable stringifyable) {
|
||||
return new PropertyValue<>(propVal.prop, propVal.rawValue, stringifyable.toShortString());
|
||||
@ -110,5 +113,12 @@ public final class Stringify<B> {
|
||||
static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) {
|
||||
return rawValue != null ? new PropertyValue<>(prop, rawValue, rawValue.toString()) : null;
|
||||
}
|
||||
|
||||
boolean nonEmpty() {
|
||||
return rawValue != null &&
|
||||
(!(rawValue instanceof Collection<?> c) || !c.isEmpty()) &&
|
||||
(!(rawValue instanceof Map<?,?> m) || !m.isEmpty()) &&
|
||||
(!(rawValue instanceof String s) || !s.isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user