Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional nested beans support for join results #94

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.hubspot.rosetta.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicate that a field is a nested optional and deserialization will follow the nested optional rules.
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@RosettaAnnotation
public @interface NestedOptional {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.hubspot.rosetta.internal;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Optional;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

public class NestedOptionalDeserializer<T> extends StdDeserializer<Optional<T>> {

private final Class<T> referencedClazz;

public NestedOptionalDeserializer(Class<Optional<T>> clazz, Class<T> referencedClazz) {
super(clazz);
this.referencedClazz = referencedClazz;
}

@Override
public Optional<T> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();

JsonNode root = mapper.readValue(jp, JsonNode.class);

return convert(root, mapper);
}

private Optional<T> convert(JsonNode root, ObjectMapper mapper) throws IOException {
if (root.isNull()) {
return Optional.empty();
}

Iterator<Entry<String, JsonNode>> it = root.fields();

if (!it.hasNext()) {
throw new IllegalArgumentException("The provided object has no fields: " + root);
}

while (it.hasNext()) {
Entry<String, JsonNode> entry = it.next();

if (!entry.getValue().isNull()) {
return Optional.of(
mapper.treeToValue(root, referencedClazz)
);
}
}

return Optional.empty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import com.fasterxml.jackson.databind.type.ReferenceType;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.hubspot.rosetta.annotations.NestedOptional;
import com.hubspot.rosetta.annotations.RosettaCreator;
import com.hubspot.rosetta.annotations.RosettaDeserialize;
import com.hubspot.rosetta.annotations.RosettaIgnore;
Expand Down Expand Up @@ -65,6 +68,7 @@ public JsonSerializer<?> findSerializer(Annotated a) {
@SuppressWarnings("unchecked")
public JsonDeserializer<?> findDeserializer(Annotated a) {
StoredAsJson storedAsJson = a.getAnnotation(StoredAsJson.class);
NestedOptional nestedOptional = a.getAnnotation(NestedOptional.class);
RosettaDeserialize rosettaDeserialize = a.getAnnotation(RosettaDeserialize.class);
if (storedAsJson != null && rosettaDeserialize != null) {
throw new IllegalArgumentException("Cannot have @StoredAsJson as well as @RosettaDeserialize annotations on the same entry");
Expand All @@ -78,6 +82,12 @@ public JsonDeserializer<?> findDeserializer(Annotated a) {
return new StoredAsJsonDeserializer(a.getRawType(), a.getType(), empty, objectMapper);
}

if (nestedOptional != null && (a instanceof AnnotatedMethod || a instanceof AnnotatedParameter)) {
ReferenceType refType = getReferenceType(a);

return new NestedOptionalDeserializer(refType.getRawClass(), refType.getReferencedType().getRawClass());
}

if (rosettaDeserialize != null) {
Class<? extends JsonDeserializer> klass = rosettaDeserialize.using();
if (klass != JsonDeserializer.None.class) {
Expand Down Expand Up @@ -170,4 +180,14 @@ private Annotated getAnnotatedTypeFromAnnotatedMethod(AnnotatedMethod a) {
throw new IllegalArgumentException("Cannot have @StoredAsJson on a method with no parameters AND no arguments");
}
}

private ReferenceType getReferenceType(Annotated a) {
if (a instanceof AnnotatedMethod) {
return (ReferenceType) ((AnnotatedMethod) a).getParameterType(0);
} else if (a instanceof AnnotatedParameter) {
return ReferenceType.upgradeFrom(a.getType(), a.getType().containedType(0));
} else {
throw new IllegalArgumentException("Could not get ReferencedType.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.hubspot.rosetta.annotations;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;

import org.junit.Test;

import com.hubspot.rosetta.Rosetta;
import com.hubspot.rosetta.beans.InnerBean;
import com.hubspot.rosetta.beans.NestedOptionalBean;

public class NestedOptionalTest {

@Test
public void testAnnotatedFieldDeserialization() throws IOException {
String nestedOptionalBeanJson = "" +
"{\"firstNestedOptional\":{\"stringProperty\":\"value-1\"}, " +
"\"secondNestedOptional\":{\"firstStringProperty\": null, \"secondStringProperty\": null}}";

NestedOptionalBean bean = Rosetta.getMapper().readValue(nestedOptionalBeanJson, NestedOptionalBean.class);

assertThat(bean.getFirstNestedOptional().map(InnerBean::getStringProperty)).contains("value-1");
assertThat(bean.getSecondNestedOptional()).isEmpty();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.hubspot.rosetta.beans;

public class MoreFieldsInnerFieldBean {
private String firstStringProperty;
private String secondStringProperty;

public String getFirstStringProperty() {
return firstStringProperty;
}

public void setFirstStringProperty(String firstStringProperty) {
this.firstStringProperty = firstStringProperty;
}

public String getSecondStringProperty() {
return secondStringProperty;
}

public void setSecondStringProperty(String secondStringProperty) {
this.secondStringProperty = secondStringProperty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.hubspot.rosetta.beans;

import java.util.Optional;

import com.hubspot.rosetta.annotations.NestedOptional;

public class NestedOptionalBean {

@NestedOptional
private Optional<InnerBean> firstNestedOptional;

@NestedOptional
private Optional<MoreFieldsInnerFieldBean> secondNestedOptional;

public Optional<InnerBean> getFirstNestedOptional() {
return firstNestedOptional;
}

public void setFirstNestedOptional(Optional<InnerBean> firstNestedOptional) {
this.firstNestedOptional = firstNestedOptional;
}

public Optional<MoreFieldsInnerFieldBean> getSecondNestedOptional() {
return secondNestedOptional;
}

public void setSecondNestedOptional(Optional<MoreFieldsInnerFieldBean> secondNestedOptional) {
this.secondNestedOptional = secondNestedOptional;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ public void setup() {
jdbi.useHandle(handle -> {
handle.execute("CREATE TABLE IF NOT EXISTS test_table (id INT, name VARCHAR(255) NOT NULL, PRIMARY KEY (id))");
handle.execute("CREATE TABLE IF NOT EXISTS test_list_table (id INT, \"value\" INT NOT NULL, PRIMARY KEY (id))");
handle.execute("CREATE TABLE IF NOT EXISTS test_nested_table (id INT, relatedId INT, otherName VARCHAR(255), score BIGINT, PRIMARY KEY (id))");
handle.execute("CREATE TABLE IF NOT EXISTS test_subtyped_nested_table (relatedId INT, color VARCHAR(255), relaxSong VARCHAR(255) DEFAULT NULL, relaxLevel BIGINT DEFAULT NULL, dangerLevel BIGINT DEFAULT NULL, PRIMARY KEY (relatedId))");
handle.execute("TRUNCATE TABLE test_table");
handle.execute("TRUNCATE TABLE test_list_table");
handle.execute("TRUNCATE TABLE test_nested_table");
handle.execute("TRUNCATE TABLE test_subtyped_nested_table");
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.hubspot.rosetta.jdbi3;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Optional;

import org.junit.Test;

public class NestedOptionalTest extends AbstractJdbiTest {

@Test
public void itDeserializesNestedObject() {
TestObject firstObject = new TestObject(1, "name-1");
TestRelatedObject relatedObject = new TestRelatedObject(1, 1, "related-name-1", 12);

TestViewObject firstExpectedView = new TestViewObject(1, "name-1", Optional.of(relatedObject));

TestObject secondObject = new TestObject(2, "name-2");

TestViewObject secondExpectedView = new TestViewObject(2, "name-2", Optional.empty());

getDao().insert(firstObject);
getDao().insert(relatedObject);

getDao().insert(secondObject);

assertThat(getDao().getAllView()).contains(firstExpectedView, secondExpectedView);
}

@Test
public void itDeserializedSubTypedNestedObject() {
TestObject firstObject = new TestObject(1, "name-1");
TestGreenNestedObject firstRelatedObject = new TestGreenNestedObject(1, 1000L, "relax-song-1");

TestObject secondObject = new TestObject(2, "name-2");
TestRedNestedObject secondRelatedObject = new TestRedNestedObject(2, 300L);

TestObject thirdObject = new TestObject(3, "name-3");

TestSubTypedViewObject firstExpectedView = new TestSubTypedViewObject(1, "name-1", Optional.of(firstRelatedObject));
TestSubTypedViewObject secondExpectedView = new TestSubTypedViewObject(2, "name-2", Optional.of(secondRelatedObject));
TestSubTypedViewObject thirdExpectedView = new TestSubTypedViewObject(3, "name-3", Optional.empty());

getDao().insert(firstObject);
getDao().insert(firstRelatedObject);

getDao().insert(secondObject);
getDao().insert(secondRelatedObject);

getDao().insert(thirdObject);

assertThat(getDao().getAllSubTypedNestedView()).contains(firstExpectedView, secondExpectedView, thirdExpectedView);
}
}
32 changes: 32 additions & 0 deletions RosettaJdbi3/src/test/java/com/hubspot/rosetta/jdbi3/TestDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,41 @@ public interface TestDao extends SqlObject {
@SqlQuery("SELECT * FROM test_list_table WHERE \"value\" IN (<values>)")
List<TestListObject> getWithObjectFieldValue(@BindListWithRosetta(value = "values", field = "objectValue") List<TestListObject> values);

@SqlQuery("SELECT " +
"test_table.id AS id, " +
"test_table.name AS name, " +
"r.id AS \"related.id\", " +
"r.relatedId AS \"related.relatedId\", " +
"r.otherName AS \"related.otherName\", " +
"r.score AS \"related.score\" " +
"FROM test_table LEFT JOIN test_nested_table as r " +
"ON test_table.id = r.relatedId;")
List<TestViewObject> getAllView();

@SqlQuery("SELECT " +
"test_table.id AS id, " +
"test_table.name AS name, " +
"subtyped.relatedId AS \"related.relatedId\", " +
"subtyped.color AS \"related.color\", " +
"subtyped.dangerLevel AS \"related.dangerLevel\", " +
"subtyped.relaxSong AS \"related.relaxSong\", " +
"subtyped.relaxLevel AS \"related.relaxLevel\" " +
"FROM test_table LEFT JOIN test_subtyped_nested_table as subtyped " +
"ON test_table.id = subtyped.relatedId;")
List<TestSubTypedViewObject> getAllSubTypedNestedView();

@SqlUpdate("INSERT INTO test_table (id, name) VALUES (:id, :name)")
int insert(@BindWithRosetta TestObject object);

@SqlUpdate("INSERT INTO test_list_table (id, \"value\") VALUES (:id, :value)")
int insert(@BindWithRosetta TestListObject object);

@SqlUpdate("INSERT INTO test_nested_table (id, relatedId, otherName, score) VALUES (:id, :relatedId, :otherName, :score);")
int insert(@BindWithRosetta TestRelatedObject object);

@SqlUpdate("INSERT INTO test_subtyped_nested_table (color, relatedId, dangerLevel) VALUES (:color, :relatedId, :dangerLevel);")
int insert(@BindWithRosetta TestRedNestedObject object);

@SqlUpdate("INSERT INTO test_subtyped_nested_table (color, relatedId, relaxSong, relaxLevel) VALUES (:color, :relatedId, :relaxSong, :relaxLevel);")
int insert(@BindWithRosetta TestGreenNestedObject object);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.hubspot.rosetta.jdbi3;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Objects;

public class TestGreenNestedObject implements TestSubTypedNestedObject {

private final int relatedId;
private final long relaxLevel;
private final String relaxSong;

@JsonCreator
public TestGreenNestedObject(@JsonProperty("relatedId") int relatedId,
@JsonProperty("relaxLevel") long relaxLevel,
@JsonProperty("relaxSong") String relaxSong) {
this.relatedId = relatedId;
this.relaxLevel = relaxLevel;
this.relaxSong = relaxSong;
}

public int getRelatedId() {
return relatedId;
}

public long getRelaxLevel() {
return relaxLevel;
}

public String getRelaxSong() {
return relaxSong;
}

@Override
public String getColor() {
return "GREEN";
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestGreenNestedObject that = (TestGreenNestedObject) o;
return relatedId == that.relatedId && relaxLevel == that.relaxLevel && Objects.equal(relaxSong, that.relaxSong);
}

@Override
public int hashCode() {
return Objects.hashCode(relatedId, relaxLevel, relaxSong);
}

@Override
public String toString() {
return "TestGreenNestedObject{" +
"relatedId=" + relatedId +
", relaxLevel=" + relaxLevel +
", relaxSong='" + relaxSong + '\'' +
'}';
}
}
Loading