feature/split-up-postalAddress #118

Merged
hsh-michaelhoennig merged 10 commits from feature/split-up-postalAddress into master 2024-11-06 12:24:44 +01:00
11 changed files with 142 additions and 54 deletions
Showing only changes of commit 74c13d94fd - Show all commits

View File

@ -54,8 +54,14 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
@Column(name = "caption") @Column(name = "caption")
private String caption; private String caption;
@Builder.Default
@Setter(AccessLevel.NONE)
@Type(JsonType.class)
@Column(name = "postaladdress") @Column(name = "postaladdress")
private String postalAddress; // multiline free-format text private Map<String, String> postalAddress = new HashMap<>();
@Transient
private PatchableMapWrapper<String> postalAddressWrapper;
@Builder.Default @Builder.Default
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@ -75,6 +81,17 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
@Transient @Transient
private PatchableMapWrapper<String> phoneNumbersWrapper; private PatchableMapWrapper<String> phoneNumbersWrapper;
public PatchableMapWrapper<String> getPostalAddress() {
return PatchableMapWrapper.of(
postalAddressWrapper,
(newWrapper) -> {postalAddressWrapper = newWrapper;},
postalAddress);
}
public void putPostalAddress(Map<String, String> newPostalAddress) {
getPostalAddress().assign(newPostalAddress);
}
public PatchableMapWrapper<String> getEmailAddresses() { public PatchableMapWrapper<String> getEmailAddresses() {
return PatchableMapWrapper.of( return PatchableMapWrapper.of(
emailAddressesWrapper, emailAddressesWrapper,

View File

@ -18,7 +18,8 @@ class HsOfficeContactEntityPatcher implements EntityPatcher<HsOfficeContactPatch
@Override @Override
public void apply(final HsOfficeContactPatchResource resource) { public void apply(final HsOfficeContactPatchResource resource) {
OptionalFromJson.of(resource.getCaption()).ifPresent(entity::setCaption); OptionalFromJson.of(resource.getCaption()).ifPresent(entity::setCaption);
OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress); Optional.ofNullable(resource.getPostalAddress())
.ifPresent(r -> entity.getPostalAddress().patch(KeyValueMap.from(resource.getPostalAddress())));
Optional.ofNullable(resource.getEmailAddresses()) Optional.ofNullable(resource.getEmailAddresses())
.ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses()))); .ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses())));
Optional.ofNullable(resource.getPhoneNumbers()) Optional.ofNullable(resource.getPhoneNumbers())

View File

@ -47,7 +47,7 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData ) OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData )
AND ( :contactData IS NULL AND ( :contactData IS NULL
OR lower(rel.contact.caption) LIKE :contactData OR lower(rel.contact.caption) LIKE :contactData
OR lower(rel.contact.postalAddress) LIKE :contactData OR lower(CAST(rel.contact.postalAddress AS String)) LIKE :contactData
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData ) OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
""") """)

View File

@ -12,7 +12,7 @@ components:
caption: caption:
type: string type: string
postalAddress: postalAddress:
type: string $ref: '#/components/schemas/HsOfficeContactPostalAddress'
emailAddresses: emailAddresses:
$ref: '#/components/schemas/HsOfficeContactEmailAddresses' $ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers: phoneNumbers:
@ -24,7 +24,7 @@ components:
caption: caption:
type: string type: string
postalAddress: postalAddress:
type: string $ref: '#/components/schemas/HsOfficeContactPostalAddress'
emailAddresses: emailAddresses:
$ref: '#/components/schemas/HsOfficeContactEmailAddresses' $ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers: phoneNumbers:
@ -39,21 +39,48 @@ components:
type: string type: string
nullable: true nullable: true
postalAddress: postalAddress:
type: string $ref: '#/components/schemas/HsOfficeContactPostalAddress'
nullable: true
emailAddresses: emailAddresses:
$ref: '#/components/schemas/HsOfficeContactEmailAddresses' $ref: '#/components/schemas/HsOfficeContactEmailAddresses'
phoneNumbers: phoneNumbers:
$ref: '#/components/schemas/HsOfficeContactPhoneNumbers' $ref: '#/components/schemas/HsOfficeContactPhoneNumbers'
HsOfficeContactPostalAddress:
# forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
anyOf:
- type: object
properties:
firma:
type: string
nullable: true
name:
type: string
nullable: true
co:
type: string
nullable: true
street:
type: string
nullable: true
zipcode:
type: string
nullable: true
city:
type: string
nullable: true
country:
type: string
nullable: true
additionalProperties: false
HsOfficeContactEmailAddresses: HsOfficeContactEmailAddresses:
# forces generating a java.lang.Object containing a Map, instead of class HsOfficeContactEmailAddresses # forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
anyOf: anyOf:
- type: object - type: object
additionalProperties: true additionalProperties: true
HsOfficeContactPhoneNumbers: HsOfficeContactPhoneNumbers:
# forces generating a java.lang.Object containing a Map, instead of class HsOfficeContactEmailAddresses # forces generating a java.lang.Object containing a Map, instead of a class with fixed properties
anyOf: anyOf:
- type: object - type: object
properties: properties:

View File

@ -9,7 +9,7 @@ create table if not exists hs_office.contact
uuid uuid unique references rbac.object (uuid) initially deferred, uuid uuid unique references rbac.object (uuid) initially deferred,
version int not null default 0, version int not null default 0,
caption varchar(128) not null, caption varchar(128) not null,
postalAddress text, postalAddress jsonb not null,
emailAddresses jsonb not null, emailAddresses jsonb not null,
phoneNumbers jsonb not null phoneNumbers jsonb not null
); );

View File

@ -11,7 +11,6 @@
create or replace procedure hs_office.contact_create_test_data(contCaption varchar) create or replace procedure hs_office.contact_create_test_data(contCaption varchar)
language plpgsql as $$ language plpgsql as $$
declare declare
postalAddr varchar;
emailAddr varchar; emailAddr varchar;
begin begin
emailAddr = 'contact-admin@' || base.cleanIdentifier(contCaption) || '.example.com'; emailAddr = 'contact-admin@' || base.cleanIdentifier(contCaption) || '.example.com';
@ -19,14 +18,18 @@ begin
perform rbac.create_subject(emailAddr); perform rbac.create_subject(emailAddr);
call base.defineContext('creating contact test-data', null, emailAddr); call base.defineContext('creating contact test-data', null, emailAddr);
postalAddr := E'Vorname Nachname\nStraße Hnr\nPLZ Stadt';
raise notice 'creating test contact: %', contCaption; raise notice 'creating test contact: %', contCaption;
insert insert
into hs_office.contact (caption, postaladdress, emailaddresses, phonenumbers) into hs_office.contact (caption, postaladdress, emailaddresses, phonenumbers)
values ( values (
contCaption, contCaption,
postalAddr, ( '{ ' ||
-- '"name": "' || contCaption || '",' ||
-- '"street": "Somewhere 1",' ||
-- '"zipcode": "12345",' ||
-- '"city": "Where-Ever",' ||
'"country": "Germany"' ||
'}')::jsonb,
('{ "main": "' || emailAddr || '" }')::jsonb, ('{ "main": "' || emailAddr || '" }')::jsonb,
('{ "phone_office": "+49 123 1234567" }')::jsonb ('{ "phone_office": "+49 123 1234567" }')::jsonb
); );

View File

@ -1111,7 +1111,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
contactRecord.getString("last_name"), contactRecord.getString("last_name"),
contactRecord.getString("firma"))); contactRecord.getString("firma")));
contact.putEmailAddresses(Map.of("main", contactRecord.getString("email"))); contact.putEmailAddresses(Map.of("main", contactRecord.getString("email")));
contact.setPostalAddress(toAddress(contactRecord)); contact.putPostalAddress(toAddress(contactRecord));
contact.putPhoneNumbers(toPhoneNumbers(contactRecord)); contact.putPhoneNumbers(toPhoneNumbers(contactRecord));
contacts.put(contactRecord.getInteger("contact_id"), contact); contacts.put(contactRecord.getInteger("contact_id"), contact);
@ -1131,36 +1131,21 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
return phoneNumbers; return phoneNumbers;
} }
private String toAddress(final Record rec) { private Map<String, String> toAddress(final Record rec) {
final var result = new StringBuilder(); final var result = new LinkedHashMap<String, String>();
final var name = toName( final var name = toName(
rec.getString("salut"), rec.getString("salut"),
rec.getString("title"), rec.getString("title"),
rec.getString("first_name"), rec.getString("first_name"),
rec.getString("last_name")); rec.getString("last_name"));
if (isNotBlank(name)) if (isNotBlank(name))
result.append(name + "\n"); result.put("name", name);
if (isNotBlank(rec.getString("firma")))
result.append(rec.getString("firma") + "\n");
if (isNotBlank(rec.getString("co")))
result.append("c/o " + rec.getString("co") + "\n");
if (isNotBlank(rec.getString("street")))
result.append(rec.getString("street") + "\n");
final var zipcodeAndCity = toZipcodeAndCity(rec);
if (isNotBlank(zipcodeAndCity))
result.append(zipcodeAndCity + "\n");
return result.toString();
}
private String toZipcodeAndCity(final Record rec) { List.of("firma", "co", "street", "zipcode", "city", "country").forEach(key -> {
final var result = new StringBuilder(); if (isNotBlank(rec.getString(key)))
if (isNotBlank(rec.getString("country"))) result.put(key, rec.getString(key));
result.append(rec.getString("country") + " "); });
if (isNotBlank(rec.getString("zipcode"))) return result;
result.append(rec.getString("zipcode") + " ");
if (isNotBlank(rec.getString("city")))
result.append(rec.getString("city"));
return result.toString();
} }
private String toCaption( private String toCaption(

View File

@ -21,10 +21,13 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
@ -214,7 +217,10 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
"emailAddresses": { "emailAddresses": {
"main": "patched@example.org" "main": "patched@example.org"
}, },
"postalAddress": "Patched Address", "postalAddress": {
"co": "P. Patcher",
"street": "Patchstraße 5"
},
"phoneNumbers": { "phoneNumbers": {
"phone_office": "+01 100 123456" "phone_office": "+01 100 123456"
} }
@ -229,7 +235,9 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body("uuid", isUuidValid()) .body("uuid", isUuidValid())
.body("caption", is("Temp patched contact")) .body("caption", is("Temp patched contact"))
.body("emailAddresses", is(Map.of("main", "patched@example.org"))) .body("emailAddresses", is(Map.of("main", "patched@example.org")))
.body("postalAddress", is("Patched Address")) .body("postalAddress", hasEntry("name", givenContact.getPostalAddress().get("name"))) // unchanged
.body("postalAddress", hasEntry("co", "P. Patcher")) // patched
.body("postalAddress", hasEntry("street", "Patchstraße 5")) // patched
.body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456"))); .body("phoneNumbers", is(Map.of("phone_office", "+01 100 123456")));
// @formatter:on // @formatter:on
@ -239,7 +247,11 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.matches(person -> { .matches(person -> {
assertThat(person.getCaption()).isEqualTo("Temp patched contact"); assertThat(person.getCaption()).isEqualTo("Temp patched contact");
assertThat(person.getEmailAddresses()).containsExactlyEntriesOf(Map.of("main", "patched@example.org")); assertThat(person.getEmailAddresses()).containsExactlyEntriesOf(Map.of("main", "patched@example.org"));
assertThat(person.getPostalAddress()).isEqualTo("Patched Address"); assertThat(person.getPostalAddress()).containsAllEntriesOf(Map.ofEntries(
entry("name", givenContact.getPostalAddress().get("name")),
entry("co", "P. Patcher"),
entry("street", "Patchstraße 5")
));
assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456")); assertThat(person.getPhoneNumbers()).containsExactlyEntriesOf(Map.of("phone_office", "+01 100 123456"));
return true; return true;
}); });
@ -264,7 +276,7 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
"phone_office": "+01 100 123456" "phone_office": "+01 100 123456"
} }
} }
""") """)
.port(port) .port(port)
.when() .when()
.patch("http://localhost/api/hs/office/contacts/" + givenContact.getUuid()) .patch("http://localhost/api/hs/office/contacts/" + givenContact.getUuid())
@ -361,8 +373,13 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
final var newContact = HsOfficeContactRbacEntity.builder() final var newContact = HsOfficeContactRbacEntity.builder()
.uuid(UUID.randomUUID()) .uuid(UUID.randomUUID())
.caption("Temp from " + Context.getCallerMethodNameFromStackFrame(1) ) .caption("Temp from " + Context.getCallerMethodNameFromStackFrame(1) )
.postalAddress(Map.ofEntries(
entry("name", RandomStringUtils.randomAlphabetic(6) + " " + RandomStringUtils.randomAlphabetic(10)),
entry("street", RandomStringUtils.randomAlphabetic(10) + randomInt(1, 99)),
entry("zipcode", "D-" + randomInt(10000, 99999)),
entry("city", RandomStringUtils.randomAlphabetic(10))
))
.emailAddresses(Map.of("main", RandomStringUtils.randomAlphabetic(10) + "@example.org")) .emailAddresses(Map.of("main", RandomStringUtils.randomAlphabetic(10) + "@example.org"))
.postalAddress("Postal Address " + RandomStringUtils.randomAlphabetic(10))
.phoneNumbers(Map.of("phone_office", "+01 200 " + RandomStringUtils.randomNumeric(8))) .phoneNumbers(Map.of("phone_office", "+01 200 " + RandomStringUtils.randomNumeric(8)))
.build(); .build();
@ -378,4 +395,8 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
em.createQuery("DELETE FROM HsOfficeContactRbacEntity c WHERE c.caption LIKE 'Temp %'").executeUpdate(); em.createQuery("DELETE FROM HsOfficeContactRbacEntity c WHERE c.caption LIKE 'Temp %'").executeUpdate();
}).assertSuccessful(); }).assertSuccessful();
} }
private int randomInt(final int min, final int max) {
return ThreadLocalRandom.current().nextInt(min, max);
}
} }

View File

@ -19,6 +19,20 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
> { > {
private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
private static final Map<String, String> PATCH_POSTAL_ADDRESS = patchMap(
entry("name", "Patty Patch"),
entry("street", "Patchstreet 10"),
entry("zipcode", null),
entry("city", "Hamburg")
);
private static final Map<String, String> PATCHED_POSTAL_ADDRESS = patchMap(
entry("name", "Patty Patch"),
entry("street", "Patchstreet 10"),
entry("zipcode", "20000"),
entry("city", "Hamburg")
);
private static final Map<String, String> PATCH_EMAIL_ADDRESSES = patchMap( private static final Map<String, String> PATCH_EMAIL_ADDRESSES = patchMap(
entry("main", "patched@example.com"), entry("main", "patched@example.com"),
entry("paul", null), entry("paul", null),
@ -46,6 +60,11 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
final var entity = new HsOfficeContactRbacEntity(); final var entity = new HsOfficeContactRbacEntity();
entity.setUuid(INITIAL_CONTACT_UUID); entity.setUuid(INITIAL_CONTACT_UUID);
entity.setCaption("initial caption"); entity.setCaption("initial caption");
entity.putPostalAddress(Map.ofEntries(
entry("name", "Ina Initial"),
entry("street", "Initialstraße 50"),
entry("zipcode", "20000"),
entry("city", "Hamburg")));
entity.putEmailAddresses(Map.ofEntries( entity.putEmailAddresses(Map.ofEntries(
entry("main", "initial@example.org"), entry("main", "initial@example.org"),
entry("paul", "paul@example.com"), entry("paul", "paul@example.com"),
@ -54,7 +73,6 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
entry("phone_office", "+49 40 12345-00"), entry("phone_office", "+49 40 12345-00"),
entry("phone_mobile", "+49 1555 1234567"), entry("phone_mobile", "+49 1555 1234567"),
entry("fax", "+49 40 12345-90"))); entry("fax", "+49 40 12345-90")));
entity.setPostalAddress("Initialstraße 50\n20000 Hamburg");
return entity; return entity;
} }
@ -77,24 +95,26 @@ class HsOfficeContactPatcherUnitTest extends PatchUnitTestBase<
"patched caption", "patched caption",
HsOfficeContactRbacEntity::setCaption), HsOfficeContactRbacEntity::setCaption),
new SimpleProperty<>( new SimpleProperty<>(
"resources", "postalAddress",
HsOfficeContactPatchResource::setEmailAddresses,
PATCH_POSTAL_ADDRESS,
HsOfficeContactRbacEntity::putEmailAddresses,
PATCHED_POSTAL_ADDRESS)
.notNullable(),
new SimpleProperty<>(
"emailAddresses",
HsOfficeContactPatchResource::setEmailAddresses, HsOfficeContactPatchResource::setEmailAddresses,
PATCH_EMAIL_ADDRESSES, PATCH_EMAIL_ADDRESSES,
HsOfficeContactRbacEntity::putEmailAddresses, HsOfficeContactRbacEntity::putEmailAddresses,
PATCHED_EMAIL_ADDRESSES) PATCHED_EMAIL_ADDRESSES)
.notNullable(), .notNullable(),
new SimpleProperty<>( new SimpleProperty<>(
"resources", "phoneNumbers",
HsOfficeContactPatchResource::setPhoneNumbers, HsOfficeContactPatchResource::setPhoneNumbers,
PATCH_PHONE_NUMBERS, PATCH_PHONE_NUMBERS,
HsOfficeContactRbacEntity::putPhoneNumbers, HsOfficeContactRbacEntity::putPhoneNumbers,
PATCHED_PHONE_NUMBERS) PATCHED_PHONE_NUMBERS)
.notNullable(), .notNullable()
new JsonNullableProperty<>(
"patched given name",
HsOfficeContactPatchResource::setPostalAddress,
"patched given name",
HsOfficeContactRbacEntity::setPostalAddress)
); );
} }
} }

View File

@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.hs.office.contact;
import java.util.Map; import java.util.Map;
import static java.util.Map.entry;
public class HsOfficeContactRbacTestEntity { public class HsOfficeContactRbacTestEntity {
public static final HsOfficeContactRbacEntity TEST_RBAC_CONTACT = hsOfficeContact("some contact", "some-contact@example.com"); public static final HsOfficeContactRbacEntity TEST_RBAC_CONTACT = hsOfficeContact("some contact", "some-contact@example.com");
@ -9,7 +11,12 @@ public class HsOfficeContactRbacTestEntity {
static public HsOfficeContactRbacEntity hsOfficeContact(final String caption, final String emailAddr) { static public HsOfficeContactRbacEntity hsOfficeContact(final String caption, final String emailAddr) {
return HsOfficeContactRbacEntity.builder() return HsOfficeContactRbacEntity.builder()
.caption(caption) .caption(caption)
.postalAddress("address of " + caption) .postalAddress(Map.ofEntries(
entry("name", "M. Meyer"),
entry("street", "Teststraße 11"),
entry("zipcode", "D-12345"),
entry("city", "Berlin")
))
.emailAddresses(Map.of("main", emailAddr)) .emailAddresses(Map.of("main", emailAddr))
.build(); .build();
} }

View File

@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.hs.office.contact;
import java.util.Map; import java.util.Map;
import static java.util.Map.entry;
public class HsOfficeContactRealTestEntity { public class HsOfficeContactRealTestEntity {
public static final HsOfficeContactRealEntity TEST_REAL_CONTACT = hsOfficeContact("some contact", "some-contact@example.com"); public static final HsOfficeContactRealEntity TEST_REAL_CONTACT = hsOfficeContact("some contact", "some-contact@example.com");
@ -9,7 +11,12 @@ public class HsOfficeContactRealTestEntity {
static public HsOfficeContactRealEntity hsOfficeContact(final String caption, final String emailAddr) { static public HsOfficeContactRealEntity hsOfficeContact(final String caption, final String emailAddr) {
return HsOfficeContactRealEntity.builder() return HsOfficeContactRealEntity.builder()
.caption(caption) .caption(caption)
.postalAddress("address of " + caption) .postalAddress(Map.ofEntries(
entry("name", "M. Meyer"),
entry("street", "Teststraße 11"),
entry("zipcode", "D-12345"),
entry("city", "Berlin")
))
.emailAddresses(Map.of("main", emailAddr)) .emailAddresses(Map.of("main", emailAddr))
.build(); .build();
} }