Skip to content

Commit

Permalink
add support to generate subschemas as nested classes
Browse files Browse the repository at this point in the history
  • Loading branch information
Stepan Kolesnik committed Mar 14, 2022
1 parent 856f5e7 commit 39636fc
Show file tree
Hide file tree
Showing 24 changed files with 545 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public class Jsonschema2PojoTask extends Task implements GenerationConfig {

private boolean includeTypeInfo = false;

private boolean useNestedClasses = false;

private boolean useInnerClassBuilders = false;

private boolean includeConstructorPropertiesAnnotation = false;
Expand Down Expand Up @@ -372,6 +374,23 @@ public void setIncludeTypeInfo(boolean includeTypeInfo) {
this.includeTypeInfo = includeTypeInfo;
}

/**
* Sets the 'useNestedClasses' property of this class.
*
* @param useNestedClasses
* Whether to use static nested classes instead of top-level ones when generating types of complex inline
* subschemas.
* <p>
* By default, complex types defined inline within a JSON schema are generated as top-level classes. This
* property allows to override the default behaviour so that complex types defined inline are generated
* as static nested classes of the main schema class.
* <p>
* Default: <code>false</code>.
*/
public void setUseNestedClasses(boolean useNestedClasses) {
this.useNestedClasses = useNestedClasses;
}

/**
* Sets the 'usePrimitives' property of this class.
*
Expand Down Expand Up @@ -994,6 +1013,11 @@ public boolean isIncludeTypeInfo()
return includeTypeInfo;
}

@Override
public boolean isUseNestedClasses() {
return useNestedClasses;
}

@Override
public boolean isUsePrimitives() {
return usePrimitives;
Expand Down
11 changes: 11 additions & 0 deletions jsonschema2pojo-ant/src/site/Jsonschema2PojoTask.html
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ <h3>Parameters</h3>
</td>
<td align="center" valign="top">No (default <code>false</code>)</td>
</tr>
<tr>
<td valign="top">useNestedClasses</td>
<td valign="top">
<p>Whether to use static nested classes instead of top-level ones when generating types for complex inline
subschemas.</p>
<p>By default, complex types defined inline within a JSON schema are generated as top-level classes. This property
allows to override the default behaviour so that complex types defined inline are generated as static nested
classes of the main schema class.</p>
</td>
<td align="center" valign="top">No (default <code>false</code>)</td>
</tr>
<tr>
<td valign="top">useInnerClassBuilders</td>
<td valign="top">Determines whether builders will be chainable setters or embedded classes when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public class Arguments implements GenerationConfig {
@Parameter(names = { "--include-type-info" }, description = "Include json type info; required to support polymorphic type handling. https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization")
private boolean includeTypeInfo = false;

@Parameter(names = { "-nc", "--use-nested-classes" }, description = "Generate all inline complex subschemas as nested classes of the main schema class. The default is one top-level class corresponding to one complex subschema.")
private boolean useNestedClasses = false;

@Parameter(names = { "--use-inner-class-builders" }, description = "Generate an inner class with builder-style methods")
private boolean useInnerClassBuilders = false;

Expand Down Expand Up @@ -310,6 +313,11 @@ public boolean isIncludeTypeInfo()
return includeTypeInfo;
}

@Override
public boolean isUseNestedClasses() {
return useNestedClasses;
}

public String getLogLevel() {
return logLevel;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public void parseRecognisesValidArguments() {
ArgsForTest args = (ArgsForTest) new ArgsForTest().parse(new String[] {
"--source", "/home/source", "--target", "/home/target", "--disable-getters", "--package", "mypackage",
"--generate-builders", "--use-primitives", "--omit-hashcode-and-equals", "--omit-tostring", "--include-dynamic-accessors",
"--include-dynamic-getters", "--include-dynamic-setters", "--include-dynamic-builders", "--inclusion-level", "ALWAYS"
"--include-dynamic-getters", "--include-dynamic-setters", "--include-dynamic-builders", "--inclusion-level", "ALWAYS",
"--use-nested-classes"
});

assertThat(args.didExit(), is(false));
Expand All @@ -71,13 +72,15 @@ public void parseRecognisesValidArguments() {
assertThat(args.isIncludeDynamicGetters(), is(true));
assertThat(args.isIncludeDynamicSetters(), is(true));
assertThat(args.isIncludeDynamicBuilders(), is(true));
assertThat(args.isUseNestedClasses(), is(true));
assertThat(args.getInclusionLevel(), is(InclusionLevel.ALWAYS));
}

@Test
public void parseRecognisesShorthandArguments() {
ArgsForTest args = (ArgsForTest) new ArgsForTest().parse(new String[] {
"-s", "/home/source", "-t", "/home/target", "-p", "mypackage", "-b", "-P", "-E", "-S", "-ida", "-idg", "-ids", "-idb", "-il", "ALWAYS"
"-s", "/home/source", "-t", "/home/target", "-p", "mypackage", "-b", "-P", "-E", "-S", "-ida", "-idg",
"-ids", "-idb", "-il", "ALWAYS", "-nc"
});

assertThat(args.didExit(), is(false));
Expand All @@ -92,6 +95,7 @@ public void parseRecognisesShorthandArguments() {
assertThat(args.isIncludeDynamicGetters(), is(true));
assertThat(args.isIncludeDynamicSetters(), is(true));
assertThat(args.isIncludeDynamicBuilders(), is(true));
assertThat(args.isUseNestedClasses(), is(true));
assertThat(args.getInclusionLevel(), is(InclusionLevel.ALWAYS));
}

Expand Down Expand Up @@ -124,6 +128,7 @@ public void allOptionalArgsCanBeOmittedAndDefaultsPrevail() {
assertThat(args.isIncludeDynamicGetters(), is(false));
assertThat(args.isIncludeDynamicSetters(), is(false));
assertThat(args.isIncludeDynamicBuilders(), is(false));
assertThat(args.isUseNestedClasses(), is(false));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ public boolean isIncludeTypeInfo()
return false;
}

/**
* @return <code>false</code>
*/
@Override
public boolean isUseNestedClasses() {
return false;
}

/**
* @return <code>false</code>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ public interface GenerationConfig {
*/
boolean isIncludeTypeInfo();

/**
* Gets the 'useNestedClasses' configuration option.
*
* @return whether to use static nested classes instead of top-level ones when generating types for complex inline
* subschemas
*/
boolean isUseNestedClasses();

/**
* Gets the 'includeConstructorPropertiesAnnotation' configuration option.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@
import org.jsonschema2pojo.util.AnnotationHelper;

import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.ClassType;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JClassContainer;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
Expand All @@ -52,7 +51,6 @@
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JOp;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;

Expand All @@ -63,7 +61,7 @@
* "http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1">http:/
* /tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1</a>
*/
public class ObjectRule implements Rule<JPackage, JType> {
public class ObjectRule implements Rule<JClassContainer, JType> {

private final RuleFactory ruleFactory;
private final ReflectionHelper reflectionHelper;
Expand All @@ -83,16 +81,15 @@ protected ObjectRule(RuleFactory ruleFactory, ParcelableHelper parcelableHelper,
* characteristics. See other implementers of {@link Rule} for details.
*/
@Override
public JType apply(String nodeName, JsonNode node, JsonNode parent, JPackage _package, Schema schema) {

JType superType = reflectionHelper.getSuperType(nodeName, node, _package, schema);
public JType apply(String nodeName, JsonNode node, JsonNode parent, JClassContainer container, Schema schema) {
JType superType = reflectionHelper.getSuperType(nodeName, node, container.getPackage(), schema);
if (superType.isPrimitive() || reflectionHelper.isFinal(superType)) {
return superType;
}

JDefinedClass jclass;
try {
jclass = createClass(nodeName, node, _package);
jclass = createClass(nodeName, node, container);
} catch (ClassAlreadyExistsException e) {
return e.getExistingClass();
}
Expand Down Expand Up @@ -187,39 +184,37 @@ private void addParcelSupport(JDefinedClass jclass) {
* new class. This node may include a 'javaType' property which
* if present will override the fully qualified name of the newly
* generated class.
* @param _package
* the package which may contain a new class after this method
* call
* @param container
* the class container (package or class) which may contain a new
* class after this method call
* @return a reference to a newly created class
* @throws ClassAlreadyExistsException
* if the given arguments cause an attempt to create a class
* that already exists, either on the classpath or in the
* current map of classes to be generated.
*/
private JDefinedClass createClass(String nodeName, JsonNode node, JPackage _package) throws ClassAlreadyExistsException {
private JDefinedClass createClass(String nodeName, JsonNode node, JClassContainer container) throws ClassAlreadyExistsException {

JDefinedClass newType;

Annotator annotator = ruleFactory.getAnnotator();

try {
if (node.has("existingJavaType")) {
String fqn = substringBefore(node.get("existingJavaType").asText(), "<");

if (isPrimitive(fqn, _package.owner())) {
throw new ClassAlreadyExistsException(primitiveType(fqn, _package.owner()));
}
if (node.has("existingJavaType")) {
String fqn = substringBefore(node.get("existingJavaType").asText(), "<");

JClass existingClass = resolveType(_package, fqn + (node.get("existingJavaType").asText().contains("<") ? "<" + substringAfter(node.get("existingJavaType").asText(), "<") : ""));
throw new ClassAlreadyExistsException(existingClass);
if (isPrimitive(fqn, container.owner())) {
throw new ClassAlreadyExistsException(primitiveType(fqn, container.owner()));
}

boolean usePolymorphicDeserialization = annotator.isPolymorphicDeserializationSupported(node);
JClass existingClass = resolveType(container, fqn + (node.get("existingJavaType").asText().contains("<") ? "<" + substringAfter(node.get("existingJavaType").asText(), "<") : ""));
throw new ClassAlreadyExistsException(existingClass);
}

try {
if (node.has("javaType")) {
String fqn = node.path("javaType").asText();

if (isPrimitive(fqn, _package.owner())) {
if (isPrimitive(fqn, container.owner())) {
throw new GenerationException("javaType cannot refer to a primitive type (" + fqn + "), did you mean to use existingJavaType?");
}

Expand All @@ -229,24 +224,21 @@ private JDefinedClass createClass(String nodeName, JsonNode node, JPackage _pack

int index = fqn.lastIndexOf(".") + 1;
if (index == 0) { //Actually not a fully qualified name
fqn = _package.name() + "." + fqn;
fqn = container.getPackage().name() + "." + fqn;
index = fqn.lastIndexOf(".") + 1;
}

if (index >= 0 && index < fqn.length()) {
if (index < fqn.length()) {
fqn = fqn.substring(0, index) + ruleFactory.getGenerationConfig().getClassNamePrefix() + fqn.substring(index) + ruleFactory.getGenerationConfig().getClassNameSuffix();
}

if (usePolymorphicDeserialization) {
newType = _package.owner()._class(JMod.PUBLIC, fqn, ClassType.CLASS);
} else {
newType = _package.owner()._class(fqn);
}
newType = container.owner()._class(fqn);
} else {
if (usePolymorphicDeserialization) {
newType = _package._class(JMod.PUBLIC, ruleFactory.getNameHelper().getUniqueClassName(nodeName, node, _package), ClassType.CLASS);
String name = ruleFactory.getNameHelper().getUniqueClassName(nodeName, node, container);
if (container instanceof JDefinedClass) {
newType = container._class(JMod.PUBLIC | JMod.STATIC, name);
} else {
newType = _package._class(ruleFactory.getNameHelper().getUniqueClassName(nodeName, node, _package));
newType = container._class(name);
}
}
} catch (JClassAlreadyExistsException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public Rule<JType, JType> getFormatRule() {
*
* @return a schema rule that can handle the "object" declaration.
*/
public Rule<JPackage, JType> getObjectRule() {
public Rule<JClassContainer, JType> getObjectRule() {
return new ObjectRule(this, new ParcelableHelper(), reflectionHelper);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public JType apply(String nodeName, JsonNode schemaNode, JsonNode parent, JClass
if (schemaNode.has("enum")) {
javaType = ruleFactory.getEnumRule().apply(nodeName, schemaNode, parent, generatableType, schema);
} else {
javaType = ruleFactory.getTypeRule().apply(nodeName, schemaNode, parent, generatableType.getPackage(), schema);
javaType = ruleFactory.getTypeRule().apply(nodeName, schemaNode, parent, generatableType, schema);
}
schema.setJavaTypeIfEmpty(javaType);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.JClassContainer;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JType;

/**
Expand Down Expand Up @@ -79,8 +80,15 @@ public JType apply(String nodeName, JsonNode node, JsonNode parent, JClassContai
JType type;

if (propertyTypeName.equals("object") || node.has("properties") && node.path("properties").size() > 0) {

type = ruleFactory.getObjectRule().apply(nodeName, node, parent, jClassContainer.getPackage(), schema);
if (!ruleFactory.getGenerationConfig().isUseNestedClasses() || schema == schema.getParent()) {
type = ruleFactory.getObjectRule().apply(nodeName, node, parent, jClassContainer.getPackage(), schema);
} else if (jClassContainer instanceof JDefinedClass) {
return ruleFactory.getObjectRule().apply(nodeName, node, parent, jClassContainer, schema);
} else {
Schema parentSchema = ruleFactory.getSchemaStore().create(schema.getParent().getId(), null);
return ruleFactory.getObjectRule().apply(nodeName, node, parent, (JClassContainer) parentSchema.getJavaType(),
schema);
}
} else if (node.has("existingJavaType")) {
String typeName = node.path("existingJavaType").asText();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.jsonschema2pojo.util;

import java.util.Iterator;

import static java.lang.Character.*;
import static javax.lang.model.SourceVersion.*;
import static org.apache.commons.lang3.StringUtils.*;
Expand All @@ -26,8 +28,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JClassContainer;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;

public class NameHelper {
Expand Down Expand Up @@ -238,11 +240,11 @@ public String getBuilderClassNameSuffix(JClass outerClass) {
return "Builder";
}

public String getUniqueClassName(String nodeName, JsonNode node, JPackage _package) {
return makeUnique(getClassName(nodeName, node, _package), _package);
public String getUniqueClassName(String nodeName, JsonNode node, JClassContainer container) {
return makeUnique(getClassName(nodeName, node, container), container);
}

public String getClassName(String nodeName, JsonNode node, JPackage _package) {
public String getClassName(String nodeName, JsonNode node, JClassContainer container) {
String prefix = generationConfig.getClassNamePrefix();
String suffix = generationConfig.getClassNameSuffix();
String fieldName = getClassName(nodeName, node);
Expand All @@ -266,13 +268,18 @@ private String createFullFieldName(String nodeName, String prefix, String suffix
return returnString;
}

private String makeUnique(String className, JPackage _package) {
private String makeUnique(String className, JClassContainer container) {
try {
JDefinedClass _class = _package._class(className);
_package.remove(_class);
JDefinedClass _class = container._class(className);
Iterator<JDefinedClass> iter = container.classes();
while (iter.hasNext()) {
if (iter.next().equals(_class)) {
iter.remove();
}
}
return className;
} catch (JClassAlreadyExistsException e) {
return makeUnique(MakeUniqueClassName.makeUnique(className), _package);
return makeUnique(MakeUniqueClassName.makeUnique(className), container);
}
}
}
Loading

0 comments on commit 39636fc

Please sign in to comment.