diff --git a/jsonschema2pojo-core/src/main/java/org/jsonschema2pojo/rules/EnumRule.java b/jsonschema2pojo-core/src/main/java/org/jsonschema2pojo/rules/EnumRule.java index e9b678d04..4510a151b 100644 --- a/jsonschema2pojo-core/src/main/java/org/jsonschema2pojo/rules/EnumRule.java +++ b/jsonschema2pojo-core/src/main/java/org/jsonschema2pojo/rules/EnumRule.java @@ -17,6 +17,7 @@ package org.jsonschema2pojo.rules; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.sun.codemodel.ClassType; import com.sun.codemodel.JAnnotationUse; import com.sun.codemodel.JBlock; @@ -27,6 +28,7 @@ import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JEnumConstant; import com.sun.codemodel.JExpr; +import com.sun.codemodel.JExpression; import com.sun.codemodel.JFieldVar; import com.sun.codemodel.JForEach; import com.sun.codemodel.JInvocation; @@ -72,7 +74,7 @@ public class EnumRule implements Rule { private static final String VALUE_FIELD_NAME = "value"; private final RuleFactory ruleFactory; - + protected EnumRule(RuleFactory ruleFactory) { this.ruleFactory = ruleFactory; } @@ -118,11 +120,28 @@ public JType apply(String nodeName, JsonNode node, JClassContainer container, Sc addInterfaces(_enum, node.get("javaInterfaces")); } addGeneratedAnnotation(_enum); - - JFieldVar valueField = addValueField(_enum); - addToString(_enum, valueField); - addEnumConstants(node.path("enum"), _enum, node.path("javaEnumNames")); - addFactoryMethod(_enum); + + // copy our node; remove the javaType as it will throw off the TypeRule for our case + ObjectNode typeNode = (ObjectNode)node.deepCopy(); + typeNode.remove("javaType"); + + // If type is specified on the enum, get a type rule for it. Otherwise, we're a string. + // (This is different from the default of Object, which is why we don't do this for every case.) + JType backingType = node.has("type") ? + ruleFactory.getTypeRule().apply(nodeName, typeNode, container, schema) : + container.owner().ref(String.class); + + JFieldVar valueField = addValueField(_enum, backingType); + + // override toString only if we have a sensible string to return + if(isString(backingType)){ + addToString(_enum, valueField); + } + + addValueMethod(_enum, valueField); + + addEnumConstants(node.path("enum"), _enum, node.path("javaEnumNames"), backingType); + addFactoryMethod(_enum, backingType); return _enum; } @@ -157,11 +176,11 @@ private JDefinedClass createEnum(JsonNode node, String nodeName, JClassContainer } } - private void addFactoryMethod(JDefinedClass _enum) { - JFieldVar quickLookupMap = addQuickLookupMap(_enum); + private void addFactoryMethod(JDefinedClass _enum, JType backingType) { + JFieldVar quickLookupMap = addQuickLookupMap(_enum, backingType); JMethod fromValue = _enum.method(JMod.PUBLIC | JMod.STATIC, _enum, "fromValue"); - JVar valueParam = fromValue.param(String.class, "value"); + JVar valueParam = fromValue.param(backingType, "value"); JBlock body = fromValue.body(); JVar constant = body.decl(_enum, "constant"); @@ -170,19 +189,26 @@ private void addFactoryMethod(JDefinedClass _enum) { JConditional _if = body._if(constant.eq(JExpr._null())); JInvocation illegalArgumentException = JExpr._new(_enum.owner().ref(IllegalArgumentException.class)); - illegalArgumentException.arg(valueParam); + JExpression expr = valueParam; + + // if string no need to add "" + if(!isString(backingType)){ + expr = expr.plus(JExpr.lit("")); + } + + illegalArgumentException.arg(expr); _if._then()._throw(illegalArgumentException); _if._else()._return(constant); ruleFactory.getAnnotator().enumCreatorMethod(fromValue); } - private JFieldVar addQuickLookupMap(JDefinedClass _enum) { + private JFieldVar addQuickLookupMap(JDefinedClass _enum, JType backingType) { - JClass lookupType = _enum.owner().ref(Map.class).narrow(_enum.owner().ref(String.class), _enum); + JClass lookupType = _enum.owner().ref(Map.class).narrow(backingType.boxify(), _enum); JFieldVar lookupMap = _enum.field(JMod.PRIVATE | JMod.STATIC | JMod.FINAL, lookupType, "CONSTANTS"); - JClass lookupImplType = _enum.owner().ref(HashMap.class).narrow(_enum.owner().ref(String.class), _enum); + JClass lookupImplType = _enum.owner().ref(HashMap.class).narrow(backingType.boxify(), _enum); lookupMap.init(JExpr._new(lookupImplType)); JForEach forEach = _enum.init().forEach(_enum, "c", JExpr.invoke("values")); @@ -193,11 +219,11 @@ private JFieldVar addQuickLookupMap(JDefinedClass _enum) { return lookupMap; } - private JFieldVar addValueField(JDefinedClass _enum) { - JFieldVar valueField = _enum.field(JMod.PRIVATE | JMod.FINAL, String.class, VALUE_FIELD_NAME); + private JFieldVar addValueField(JDefinedClass _enum, JType type) { + JFieldVar valueField = _enum.field(JMod.PRIVATE | JMod.FINAL, type, VALUE_FIELD_NAME); JMethod constructor = _enum.constructor(JMod.PRIVATE); - JVar valueParam = constructor.param(String.class, VALUE_FIELD_NAME); + JVar valueParam = constructor.param(type, VALUE_FIELD_NAME); JBlock body = constructor.body(); body.assign(JExpr._this().ref(valueField), valueParam); @@ -208,13 +234,30 @@ private void addToString(JDefinedClass _enum, JFieldVar valueField) { JMethod toString = _enum.method(JMod.PUBLIC, String.class, "toString"); JBlock body = toString.body(); - body._return(JExpr._this().ref(valueField)); + JExpression toReturn = JExpr._this().ref(valueField); + if(!isString(valueField.type())){ + toReturn = toReturn.plus(JExpr.lit("")); + } + + body._return(toReturn); - ruleFactory.getAnnotator().enumValueMethod(toString); toString.annotate(Override.class); } + + private void addValueMethod(JDefinedClass _enum, JFieldVar valueField) { + JMethod fromValue = _enum.method(JMod.PUBLIC, valueField.type(), "value"); - private void addEnumConstants(JsonNode node, JDefinedClass _enum, JsonNode customNames) { + JBlock body = fromValue.body(); + body._return(JExpr._this().ref(valueField)); + + ruleFactory.getAnnotator().enumValueMethod(fromValue); + } + + private boolean isString(JType type){ + return type.fullName().equals(String.class.getName()); + } + + private void addEnumConstants(JsonNode node, JDefinedClass _enum, JsonNode customNames, JType type) { Collection existingConstantNames = new ArrayList(); for (int i = 0; i < node.size(); i++) { JsonNode value = node.path(i); @@ -225,7 +268,20 @@ private void addEnumConstants(JsonNode node, JDefinedClass _enum, JsonNode custo existingConstantNames.add(constantName); JEnumConstant constant = _enum.enumConstant(constantName); - constant.arg(JExpr.lit(value.asText())); + + String typeName = type.unboxify().fullName(); + if(typeName.equals("int")){ // integer + constant.arg(JExpr.lit(value.intValue())); + } else if(typeName.equals("long")){ // integer-as-long + constant.arg(JExpr.lit(value.longValue())); + } else if(typeName.equals("double")){ // number + constant.arg(JExpr.lit(value.doubleValue())); + } else if(typeName.equals("boolean")){ // boolean + constant.arg(JExpr.lit(value.booleanValue())); + } else { // string, null, array, object? + // only string should really be valid here... TODO throw error? + constant.arg(JExpr.lit(value.asText())); + } ruleFactory.getAnnotator().enumConstant(constant, value.asText()); } } diff --git a/jsonschema2pojo-core/src/test/java/org/jsonschema2pojo/rules/EnumRuleTest.java b/jsonschema2pojo-core/src/test/java/org/jsonschema2pojo/rules/EnumRuleTest.java index d454c889c..1f1510106 100644 --- a/jsonschema2pojo-core/src/test/java/org/jsonschema2pojo/rules/EnumRuleTest.java +++ b/jsonschema2pojo-core/src/test/java/org/jsonschema2pojo/rules/EnumRuleTest.java @@ -45,6 +45,7 @@ public class EnumRuleTest { private NameHelper nameHelper = mock(NameHelper.class); private Annotator annotator = mock(Annotator.class); private RuleFactory ruleFactory = mock(RuleFactory.class); + private TypeRule typeRule = mock(TypeRule.class); private EnumRule rule = new EnumRule(ruleFactory); @@ -52,6 +53,7 @@ public class EnumRuleTest { public void wireUpConfig() { when(ruleFactory.getNameHelper()).thenReturn(nameHelper); when(ruleFactory.getAnnotator()).thenReturn(annotator); + when(ruleFactory.getTypeRule()).thenReturn(typeRule); } @Test @@ -71,6 +73,10 @@ public void applyGeneratesUniqueEnumNamesForMultipleEnumNodesWithSameName() { ObjectNode enumNode = objectMapper.createObjectNode(); enumNode.put("type", "string"); enumNode.put("enum", arrayNode); + + // We're always a string for the purposes of this test + when(typeRule.apply("status", enumNode, jpackage, schema)) + .thenReturn(jpackage.owner()._ref(String.class)); JType result1 = rule.apply("status", enumNode, jpackage, schema); JType result2 = rule.apply("status", enumNode, jpackage, schema); diff --git a/jsonschema2pojo-integration-tests/src/test/java/org/jsonschema2pojo/integration/EnumIT.java b/jsonschema2pojo-integration-tests/src/test/java/org/jsonschema2pojo/integration/EnumIT.java index 9a56b5546..e85a0ba81 100644 --- a/jsonschema2pojo-integration-tests/src/test/java/org/jsonschema2pojo/integration/EnumIT.java +++ b/jsonschema2pojo-integration-tests/src/test/java/org/jsonschema2pojo/integration/EnumIT.java @@ -25,6 +25,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.hamcrest.core.IsInstanceOf; import org.jsonschema2pojo.integration.util.Jsonschema2PojoRule; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -33,6 +34,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -79,7 +82,7 @@ public void enumClassIncludesCorrectlyNamedConstants() { @Test public void enumContainsWorkingAnnotatedSerializationMethod() throws NoSuchMethodException { - Method toString = enumClass.getMethod("toString"); + Method toString = enumClass.getMethod("value"); assertThat(enumClass.getEnumConstants()[0].toString(), is("one")); assertThat(enumClass.getEnumConstants()[1].toString(), is("secondOne")); @@ -128,7 +131,33 @@ public void enumAtRootCreatesATopLevelType() throws ClassNotFoundException, NoSu assertThat(isPublic(rootEnumClass.getModifiers()), is(true)); } + + @Test + @SuppressWarnings("unchecked") + public void intEnumAtRootCreatesIntBackedEnum() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/integerEnumAsRoot.json", "com.example"); + + Class rootEnumClass = (Class) resultsClassLoader.loadClass("com.example.enums.IntegerEnumAsRoot"); + + assertThat(rootEnumClass.isEnum(), is(true)); + assertThat(rootEnumClass.getDeclaredMethod("fromValue", Integer.class), is(notNullValue())); + assertThat(isPublic(rootEnumClass.getModifiers()), is(true)); + } + + @Test + @SuppressWarnings("unchecked") + public void doubleEnumAtRootCreatesIntBackedEnum() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/doubleEnumAsRoot.json", "com.example"); + Class rootEnumClass = (Class) resultsClassLoader.loadClass("com.example.enums.DoubleEnumAsRoot"); + + assertThat(rootEnumClass.isEnum(), is(true)); + assertThat(rootEnumClass.getDeclaredMethod("fromValue", Double.class), is(notNullValue())); + assertThat(isPublic(rootEnumClass.getModifiers()), is(true)); + } + @Test @SuppressWarnings("unchecked") public void enumWithEmptyStringAsValue() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { @@ -226,7 +255,65 @@ public void enumWithCustomJavaNames() throws ClassNotFoundException, NoSuchMetho assertThat(jsonTree.get("enum_Property").isTextual(), is(true)); assertThat(jsonTree.get("enum_Property").asText(), is("3")); } + + @Test + @SuppressWarnings("unchecked") + public void intEnumIsDeserializedCorrectly() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, JsonParseException, JsonMappingException, IOException { + + ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/integerEnumToSerialize.json", "com.example"); + + // the schema for a valid instance + Class typeWithEnumProperty = resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize"); + Class enumClass = (Class) resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize$TestEnum"); + + // read the instance into the type + ObjectMapper objectMapper = new ObjectMapper(); + Object valueWithEnumProperty = objectMapper.readValue("{\"testEnum\" : 2}", typeWithEnumProperty); + + Method getEnumMethod = typeWithEnumProperty.getDeclaredMethod("getTestEnum"); + Method getValueMethod = enumClass.getDeclaredMethod("value"); + + // call getTestEnum on the value + assertThat(getEnumMethod, is(notNullValue())); + Object enumObject = getEnumMethod.invoke(valueWithEnumProperty); + + // assert that the object returned is a) a TestEnum, and b) calling .value() on it returns 2 + // as per the json snippet above + assertThat(enumObject, IsInstanceOf.instanceOf(enumClass)); + assertThat(getValueMethod, is(notNullValue())); + assertThat((Integer)getValueMethod.invoke(enumObject), is(2)); + } + @Test + @SuppressWarnings("unchecked") + public void intEnumIsSerializedCorrectly() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, JsonParseException, JsonMappingException, IOException, InstantiationException { + + ClassLoader resultsClassLoader = schemaRule.generateAndCompile("/schema/enum/integerEnumToSerialize.json", "com.example"); + + // the schema for a valid instance + Class typeWithEnumProperty = resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize"); + Class enumClass = (Class) resultsClassLoader.loadClass("com.example.enums.IntegerEnumToSerialize$TestEnum"); + + // create an instance + Object valueWithEnumProperty = typeWithEnumProperty.newInstance(); + Method enumSetter = typeWithEnumProperty.getMethod("setTestEnum", enumClass); + + // call setTestEnum(TestEnum.ONE) + enumSetter.invoke(valueWithEnumProperty, enumClass.getEnumConstants()[0]); + + ObjectMapper objectMapper = new ObjectMapper(); + + // write our instance out to json + String jsonString = objectMapper.writeValueAsString(valueWithEnumProperty); + JsonNode jsonTree = objectMapper.readTree(jsonString); + + assertThat(jsonTree.size(), is(1)); + assertThat(jsonTree.has("testEnum"), is(true)); + assertThat(jsonTree.get("testEnum").isIntegralNumber(), is(true)); + assertThat(jsonTree.get("testEnum").asInt(), is(1)); + } + + @Test @SuppressWarnings("unchecked") public void jacksonCanMarshalEnums() throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException { diff --git a/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/doubleEnumAsRoot.json b/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/doubleEnumAsRoot.json new file mode 100644 index 000000000..9f789d61b --- /dev/null +++ b/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/doubleEnumAsRoot.json @@ -0,0 +1,6 @@ +{ + "javaType" : "com.example.enums.DoubleEnumAsRoot", + "type" : "number", + "enum" : [1.0, 2.5, 3], + "javaEnumNames" : ["One", "TwoAndAHalf", "Three"] +} \ No newline at end of file diff --git a/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/integerEnumAsRoot.json b/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/integerEnumAsRoot.json new file mode 100644 index 000000000..81ff18014 --- /dev/null +++ b/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/integerEnumAsRoot.json @@ -0,0 +1,6 @@ +{ + "javaType" : "com.example.enums.IntegerEnumAsRoot", + "type" : "integer", + "enum" : [1, 2, 3], + "javaEnumNames" : ["One", "Two", "Three"] +} \ No newline at end of file diff --git a/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/integerEnumToSerialize.json b/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/integerEnumToSerialize.json new file mode 100644 index 000000000..bc9adee9f --- /dev/null +++ b/jsonschema2pojo-integration-tests/src/test/resources/schema/enum/integerEnumToSerialize.json @@ -0,0 +1,10 @@ +{ + "javaType" : "com.example.enums.IntegerEnumToSerialize", + "properties" : { + "testEnum" : { + "type" : "integer", + "enum" : [1, 2, 3], + "javaEnumNames" : ["One", "Two", "Three"] + } + } +} \ No newline at end of file