Skip to content

Commit

Permalink
Adding support to update OneToMany associations (#437)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
scriptom authored Feb 12, 2024
1 parent 4ce285c commit 669ded5
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 26 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.bullhorn</groupId>
<artifactId>sdk-rest</artifactId>
<version>2.3.3</version>
<version>2.3.4</version>
<packaging>jar</packaging>

<name>Bullhorn REST SDK</name>
Expand Down
23 changes: 5 additions & 18 deletions src/main/java/com/bullhornsdk/data/api/StandardBullhornData.java
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ public <C extends CrudResponse, T extends UpdateEntity> C updateEntity(T entity)
*/
@Override
public <C extends CrudResponse, T extends UpdateEntity> C updateEntity(T entity, Set<String> nullBypassFields) {
return this.handleUpdateEntityWithNullBypass(entity, nullBypassFields);
return this.handleUpdateEntity(entity, nullBypassFields);
}


Expand Down Expand Up @@ -1210,32 +1210,19 @@ protected FastFindListWrapper handleFastFindForEntities(String query, FastFindPa
* @return a UpdateResponse
*/
protected <C extends CrudResponse, T extends UpdateEntity> C handleUpdateEntity(T entity) {
Map<String, String> 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);
}

/**
* Makes the "entity" api call for updating entities, allowing for null bypassing
* <p>
* 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 extends CrudResponse, T extends UpdateEntity> C handleUpdateEntityWithNullBypass(T entity, Set<String> nullBypassFields) {
protected <C extends CrudResponse, T extends UpdateEntity> C handleUpdateEntity(T entity, Set<String> nullBypassFields) {
Map<String, String> uriVariables = restUriVariablesFactory.getUriVariablesForEntityUpdate(
BullhornEntityInfo.getTypesRestEntityName(entity.getClass()), entity.getId());
String url = restUrlFactory.assembleEntityUrlForUpdate();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -89,7 +92,10 @@ public <T extends BullhornEntity> String convertEntityToJsonString(T entity) {
public <T extends BullhornEntity> String convertEntityToJsonString(T entity, Set<String> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OneToMany<? extends BullhornEntity>>) (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;
}
}
Original file line number Diff line number Diff line change
@@ -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<OneToMany<? extends BullhornEntity>> {
protected ReplaceAllSerializer() {
super((Class<OneToMany<? extends BullhornEntity>>) (Class<?>) OneToMany.class);
}

@Override
public void serialize(OneToMany<? extends BullhornEntity> 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();
}
}
7 changes: 6 additions & 1 deletion src/main/java/com/bullhornsdk/data/util/ReadOnly.java
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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\" }}";

Expand Down Expand Up @@ -55,22 +57,23 @@ 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
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
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
Expand Down Expand Up @@ -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");
}
}

0 comments on commit 669ded5

Please sign in to comment.