From 669ded54ba166dd44a053c706978f33d851dc528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20El=20Fakih?= Date: Mon, 12 Feb 2024 15:10:45 -0400 Subject: [PATCH] Adding support to update OneToMany associations (#437) * Adding support to update OneToMany associations * Additional test to account for already existing RestOneToManySerializer * Renaming OneToManySerializer to ReplaceAllSerializer Done to avoid confusion with already existing RestOneToManySerializer, which serializes OneToMany to JSON Arrays. * Bump version * Make ReadOnly annotation work by changing the property access --- pom.xml | 2 +- .../data/api/StandardBullhornData.java | 23 ++------ .../data/api/helper/RestJsonConverter.java | 8 ++- .../json/replaceall/ReplaceAllModule.java | 24 ++++++++ .../json/replaceall/ReplaceAllSerializer.java | 24 ++++++++ .../com/bullhornsdk/data/util/ReadOnly.java | 7 ++- .../api/helper/RestJsonConverterTest.java | 59 +++++++++++++++++-- 7 files changed, 121 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllModule.java create mode 100644 src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllSerializer.java diff --git a/pom.xml b/pom.xml index 30c8544d..1ae04efd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.bullhorn sdk-rest - 2.3.3 + 2.3.4 jar Bullhorn REST SDK diff --git a/src/main/java/com/bullhornsdk/data/api/StandardBullhornData.java b/src/main/java/com/bullhornsdk/data/api/StandardBullhornData.java index d4ecadf2..40083f6c 100644 --- a/src/main/java/com/bullhornsdk/data/api/StandardBullhornData.java +++ b/src/main/java/com/bullhornsdk/data/api/StandardBullhornData.java @@ -356,7 +356,7 @@ public C updateEntity(T entity) */ @Override public C updateEntity(T entity, Set nullBypassFields) { - return this.handleUpdateEntityWithNullBypass(entity, nullBypassFields); + return this.handleUpdateEntity(entity, nullBypassFields); } @@ -1210,20 +1210,7 @@ protected FastFindListWrapper handleFastFindForEntities(String query, FastFindPa * @return a UpdateResponse */ protected C handleUpdateEntity(T entity) { - Map uriVariables = restUriVariablesFactory.getUriVariablesForEntityUpdate( - BullhornEntityInfo.getTypesRestEntityName(entity.getClass()), entity.getId()); - String url = restUrlFactory.assembleEntityUrlForUpdate(); - - CrudResponse response; - - try { - String jsonString = restJsonConverter.convertEntityToJsonString(entity); - response = this.performPostRequest(url, jsonString, UpdateResponse.class, uriVariables); - } catch (HttpStatusCodeException error) { - response = restErrorHandler.handleHttpFourAndFiveHundredErrors(new UpdateResponse(), error, entity.getId()); - } - - return (C) response; + return handleUpdateEntity(entity, null); } /** @@ -1231,11 +1218,11 @@ protected C handleUpdateEntity( *

* HTTP Method: POST * - * @param entity - * @param nullBypassFields + * @param entity The entity to POST to the REST API + * @param nullBypassFields The fields that should be allowed null values * @return a UpdateResponse */ - protected C handleUpdateEntityWithNullBypass(T entity, Set nullBypassFields) { + protected C handleUpdateEntity(T entity, Set nullBypassFields) { Map uriVariables = restUriVariablesFactory.getUriVariablesForEntityUpdate( BullhornEntityInfo.getTypesRestEntityName(entity.getClass()), entity.getId()); String url = restUrlFactory.assembleEntityUrlForUpdate(); diff --git a/src/main/java/com/bullhornsdk/data/api/helper/RestJsonConverter.java b/src/main/java/com/bullhornsdk/data/api/helper/RestJsonConverter.java index 4da65a7c..724d5778 100644 --- a/src/main/java/com/bullhornsdk/data/api/helper/RestJsonConverter.java +++ b/src/main/java/com/bullhornsdk/data/api/helper/RestJsonConverter.java @@ -1,6 +1,8 @@ package com.bullhornsdk.data.api.helper; import com.bullhornsdk.data.api.helper.json.DynamicNullValueFilter; +import com.bullhornsdk.data.api.helper.json.replaceall.ReplaceAllModule; +import com.fasterxml.jackson.databind.ObjectWriter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.bullhornsdk.data.exception.RestMappingException; @@ -44,6 +46,7 @@ public RestJsonConverter() { private ObjectMapper createObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JodaModule()); + mapper.registerModule(new ReplaceAllModule()); mapper.configure(SerializationFeature.INDENT_OUTPUT, true); mapper.setFilterProvider(createFieldFilter(Collections.emptySet())); return mapper; @@ -89,7 +92,10 @@ public String convertEntityToJsonString(T entity) { public String convertEntityToJsonString(T entity, Set nullBypassFields) { String jsonString = ""; try { - jsonString = this.objectMapper.writer(createFieldFilter(nullBypassFields)).writeValueAsString(entity); + ObjectWriter writer = nullBypassFields == null + ? this.objectMapper.writer() + : this.objectMapper.writer(createFieldFilter(nullBypassFields)); + jsonString = writer.writeValueAsString(entity); } catch (JsonProcessingException e) { log.error("Error deserializing entity of type" + entity.getClass() + " to jsonString.", e); } diff --git a/src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllModule.java b/src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllModule.java new file mode 100644 index 00000000..a9a691d5 --- /dev/null +++ b/src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllModule.java @@ -0,0 +1,24 @@ +package com.bullhornsdk.data.api.helper.json.replaceall; + +import com.bullhornsdk.data.model.entity.core.type.BullhornEntity; +import com.bullhornsdk.data.model.entity.embedded.OneToMany; +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class ReplaceAllModule extends SimpleModule { + public ReplaceAllModule() { + addSerializer((Class>) (Class) OneToMany.class, new ReplaceAllSerializer()); + + } + + public String getModuleName() { + return this.getClass().getSimpleName(); + } + + public int hashCode() { + return this.getClass().hashCode(); + } + + public boolean equals(Object o) { + return this == o; + } +} diff --git a/src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllSerializer.java b/src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllSerializer.java new file mode 100644 index 00000000..871a9cd0 --- /dev/null +++ b/src/main/java/com/bullhornsdk/data/api/helper/json/replaceall/ReplaceAllSerializer.java @@ -0,0 +1,24 @@ +package com.bullhornsdk.data.api.helper.json.replaceall; + +import com.bullhornsdk.data.model.entity.core.type.BullhornEntity; +import com.bullhornsdk.data.model.entity.embedded.OneToMany; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; + +public class ReplaceAllSerializer extends StdSerializer> { + protected ReplaceAllSerializer() { + super((Class>) (Class) OneToMany.class); + } + + @Override + public void serialize(OneToMany value, JsonGenerator gen, SerializerProvider provider) throws IOException { + int[] ids = value.getData().stream().mapToInt(BullhornEntity::getId).toArray(); + gen.writeStartObject(); + gen.writeFieldName("replaceAll"); + gen.writeArray(ids, 0, ids.length); + gen.writeEndObject(); + } +} diff --git a/src/main/java/com/bullhornsdk/data/util/ReadOnly.java b/src/main/java/com/bullhornsdk/data/util/ReadOnly.java index f8791973..ef3e3184 100644 --- a/src/main/java/com/bullhornsdk/data/util/ReadOnly.java +++ b/src/main/java/com/bullhornsdk/data/util/ReadOnly.java @@ -1,11 +1,16 @@ package com.bullhornsdk.data.util; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) +@Target({ElementType.METHOD, ElementType.FIELD}) +@JacksonAnnotationsInside +@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public @interface ReadOnly { } diff --git a/src/test/java/com/bullhornsdk/data/api/helper/RestJsonConverterTest.java b/src/test/java/com/bullhornsdk/data/api/helper/RestJsonConverterTest.java index 451c875d..03524d70 100644 --- a/src/test/java/com/bullhornsdk/data/api/helper/RestJsonConverterTest.java +++ b/src/test/java/com/bullhornsdk/data/api/helper/RestJsonConverterTest.java @@ -2,8 +2,9 @@ import com.bullhornsdk.data.BaseTest; import com.bullhornsdk.data.exception.RestMappingException; -import com.bullhornsdk.data.model.entity.core.standard.Candidate; -import com.bullhornsdk.data.model.entity.core.standard.JobSubmission; +import com.bullhornsdk.data.model.entity.core.customobjectinstances.placement.PlacementCustomObjectInstance1; +import com.bullhornsdk.data.model.entity.core.standard.*; +import com.bullhornsdk.data.model.entity.embedded.OneToMany; import com.bullhornsdk.data.model.enums.BullhornEntityInfo; import com.bullhornsdk.data.model.response.file.standard.StandardFileContent; import com.bullhornsdk.data.model.response.list.ListWrapper; @@ -17,14 +18,15 @@ import java.util.List; -import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; public class RestJsonConverterTest extends BaseTest { private RestJsonConverter restJsonConverter = new RestJsonConverter(); private Candidate candidate; + private Note note; private final String singleEntityJson = "{ \"data\": { \"id\": 1, \"status\":\"Approved\" }}"; @@ -55,6 +57,7 @@ public class RestJsonConverterTest extends BaseTest { @BeforeEach public void setUp() { candidate = bullhornData.findEntity(Candidate.class, testEntities.getCandidateId(), Sets.newHashSet("id", "firstName")); + note = bullhornData.findEntity(Note.class, testEntities.getNoteId(), Sets.newHashSet("id")); } @Test @@ -62,7 +65,7 @@ public void testConvertEntityToJsonString() { RestJsonConverter jsonConverter = new RestJsonConverter(); JSONObject expected = new JSONObject("{\"id\": 1,\"firstName\": \"Want\"}"); JSONObject result = new JSONObject(jsonConverter.convertEntityToJsonString(candidate)); - assertTrue("JSON conversion includes unexpected fields, or does not include expected fields", expected.similar(result)); + assertTrue(expected.similar(result), "JSON conversion includes unexpected fields, or does not include expected fields"); } @Test @@ -70,7 +73,7 @@ public void testConvertEntityToJsonStringWithNullBypass() { RestJsonConverter jsonConverter = new RestJsonConverter(); JSONObject expected = new JSONObject("{\"id\": 1,\"firstName\": \"Want\", \"lastName\": null}"); JSONObject result = new JSONObject(jsonConverter.convertEntityToJsonString(candidate, Sets.newHashSet("lastName"))); - assertTrue("JSON conversion includes unexpected fields, or does not include expected fields", expected.similar(result)); + assertTrue(expected.similar(result), "JSON conversion includes unexpected fields, or does not include expected fields"); } @Test @@ -154,4 +157,50 @@ public void testStandardFileContentWithExtraProp() { assertEquals("SomeContent", file.getFileContent()); assertEquals("FileName", file.getName()); } + + @Test + public void testOneToManySerializesToReplaceAll() { + note.setPlacements(new OneToMany<>(new Placement(1))); + JSONObject actual = new JSONObject(this.restJsonConverter.convertEntityToJsonString(note)); + JSONObject expected = new JSONObject("{\"placements\": {\"replaceAll\": [1]}, \"id\": 1}"); + assertTrue(actual.similar(expected), "JSON conversion did not conform to replaceAll standard"); + } + + @Test + public void testOneToManySerializesToReplaceAllOnEmptyArray() { + note.setPlacements(new OneToMany<>()); + JSONObject actual = new JSONObject(this.restJsonConverter.convertEntityToJsonString(note)); + JSONObject expected = new JSONObject("{\"placements\": {\"replaceAll\": []}, \"id\": 1}"); + assertTrue(actual.similar(expected), "JSON conversion did not conform to replaceAll standard"); + } + + @Test + public void testOneToManySerializesToReplaceAllOnMultipleArray() { + note.setPlacements(new OneToMany<>(new Placement(1), new Placement(2), new Placement(3))); + JSONObject actual = new JSONObject(this.restJsonConverter.convertEntityToJsonString(note)); + JSONObject expected = new JSONObject("{\"placements\": {\"replaceAll\": [1, 2, 3]}, \"id\": 1}"); + assertTrue(actual.similar(expected), "JSON conversion did not conform to replaceAll standard"); + } + + @Test + public void testRestOneToManySerializerDoesNotCollideWithReplaceAll() { + Placement placement = new Placement(1); + PlacementCustomObjectInstance1 placementCustomObjectInstance1 = new PlacementCustomObjectInstance1(); + placementCustomObjectInstance1.setId(2); + placementCustomObjectInstance1.setText1("Test"); + placement.setCustomObject1s(new OneToMany<>(placementCustomObjectInstance1)); + JSONObject actual = new JSONObject(this.restJsonConverter.convertEntityToJsonString(placement)); + JSONObject expected = new JSONObject("{\"id\": 1, \"customObject1s\": [{\"id\": 2, \"text1\": \"Test\"}]}"); + assertTrue(actual.similar(expected), "OneToMany replaceAll serializer collided with RestOneToManySerializer"); + } + + @Test + public void testReadOnlyAnnotationIgnoresFieldOnSerialization() { + JobOrder jobOrder = new JobOrder(1); + Placement placement = new Placement(2); + jobOrder.setPlacements(new OneToMany<>(placement)); + JSONObject actual = new JSONObject(this.restJsonConverter.convertEntityToJsonString(jobOrder)); + JSONObject expected = new JSONObject("{\"id\": 1}"); + assertTrue(actual.similar(expected), "ReadOnly annotated field was included in payload"); + } }