diff --git a/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiGenerator.java b/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiGenerator.java index a4c686b6a..75c3b302a 100644 --- a/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiGenerator.java +++ b/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiGenerator.java @@ -225,137 +225,7 @@ public void generate(final String targetPath, Object model, Any config) throws I ArrayList modelCreators = new ArrayList<>(); final HashMap references = new HashMap<>(); for (Map.Entry entry : schemas.asMap().entrySet()) { - final List> props = new ArrayList<>(); - String key = entry.getKey(); - Map value = entry.getValue().asMap(); - String type = null; - String enums = null; - boolean isEnum = false; - boolean isEnumClass = false; - // Map properties = null; - List required = null; - - // iterate through each schema in the components - for (Map.Entry entrySchema : value.entrySet()) { - if ("type".equals(entrySchema.getKey())) { - type = entrySchema.getValue().toString(); - if ("enum".equals(type)) { - isEnum = true; - } - } - if ("enum".equals(entrySchema.getKey())) { - isEnumClass = true; - enums = entrySchema.getValue().asList().toString(); - enums = enums.substring(enums.indexOf("[") + 1, enums.indexOf("]")); - } - if ("properties".equals(entrySchema.getKey())) { - handleProperties(props, entrySchema.getValue().asMap()); - } - if ("required".equals(entrySchema.getKey())) { - required = entrySchema.getValue().asList(); - } - if ("allOf".equals(entrySchema.getKey())) { - type = "object"; - - // could be referred to as "$ref" references or listed in "properties" - for (Any listItem : entrySchema.getValue().asList()) { - //Map allOfItem = (Map)listItem.asMap().entrySet(); - - for (Map.Entry allOfItem : listItem.asMap().entrySet()) { - if ("$ref".equals(allOfItem.getKey())) { - String s = allOfItem.getValue().toString(); - s = s.substring(s.lastIndexOf('/') + 1); - if (schemas.get(s).keys().contains("properties")) { - handleProperties(props, schemas.get(s).get("properties").asMap()); - } - } - if ("properties".equals(allOfItem.getKey())) { - handleProperties(props, allOfItem.getValue().asMap()); - } - } - } - } - } - final String classVarName = key; - final String modelFileName = key.substring(0, 1).toUpperCase() + key.substring(1); - //System.out.println("props = " + Any.wrap(props)); - - // Check the type of current schema. Generation will be executed only if the type of the schema equals to object. - // Since generate a model for primitive types and arrays do not make sense, and an error class would be generated - // due to lack of properties if force to generate. - if (type == null) { - throw new RuntimeException("Cannot find the type of \"" + modelFileName + "\" in #/components/schemas/ of the specification file."); - } - - if ("object".equals(type) || isEnumClass) { - if (!overwriteModel && checkExist(targetPath, ("src.main.java." + modelPackage).replace(".", separator), modelFileName + ".java")) { - continue; - } - - final String enumsIfClass = isEnumClass ? enums : null; - modelCreators.add(() -> { - final int referencesCount = references.size(); - for (Map properties : props) { - Any any = properties.get("type"); - if (any != null) { - if (any.valueType() == ValueType.STRING) { - Any resolved = references.get(any.toString()); - if (resolved == null) { - continue; - } - any = new UnresolvedTypeHolderAny(resolved); - properties.put("type", any); - } - - int iteration = 0; - do { - UnresolvedTypeAny previous = null; - while (any instanceof UnresolvedTypeAny) { - previous = (UnresolvedTypeAny)any; - any = ((UnresolvedTypeAny)any).get(); - } - - if (any == null) { - break; - } else if (iteration++ > referencesCount) { - throw new TypeNotPresentException(any.toString(), null); - } - - if (any.valueType() == ValueType.STRING) { - any = references.get(any.toString()); - if (any == null) { - break; - } else { - previous.set(any); - } - } else { - break; - } - } while (true); - } - } - - try { - transfer(targetPath, - ("src.main.java." + modelPackage).replace(".", separator), - modelFileName + ".java", - enumsIfClass == null - ? templates.rest.pojo.template(modelPackage, modelFileName, classVarName, props) - : templates.rest.enumClass.template(modelPackage, modelFileName, enumsIfClass)); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - }); - } else { - HashMap map = new HashMap<>(1); - map.put(key, Any.wrap(value)); - handleProperties(props, map); - if (props.isEmpty()) { - throw new IllegalStateException("Properties empty for " + classVarName + "!"); - } - - references.put(modelFileName, props.get(0).get("type")); - } + loadModel(entry.getKey(), null, entry.getValue().asMap(), schemas, overwriteModel, targetPath, modelPackage, modelCreators, references, null); } for (Runnable r : modelCreators) { @@ -926,4 +796,159 @@ private Map populateResponseExample(Operation operation) { private static final Logger logger = LoggerFactory.getLogger(OpenApiGenerator.class); + private void loadModel(String classVarName, String parentClassName, Map value, Any schemas, boolean overwriteModel, String targetPath, String modelPackage, List modelCreators, Map references, List> parentClassProps) throws IOException { + final String modelFileName = classVarName.substring(0, 1).toUpperCase() + classVarName.substring(1); + final List> props = new ArrayList<>(); + final List> parentProps = (parentClassProps == null) ? new ArrayList<>() : new ArrayList<>(parentClassProps); + String type = null; + String enums = null; + boolean isEnumClass = false; + List required = null; + boolean isAbstractClass = false; + + // iterate through each schema in the components + Queue> schemaElementQueue = new LinkedList<>(); + // cache the visited elements to prevent loop reference + Set seen = new HashSet<>(); + // add elements into queue to perform a BFS + for (Map.Entry entrySchema : value.entrySet()) { + schemaElementQueue.offer(entrySchema); + } + while (!schemaElementQueue.isEmpty()) { + Map.Entry currentElement = schemaElementQueue.poll(); + String currentElementKey = currentElement.getKey(); + // handle the base elements + if ("type".equals(currentElementKey) && type == null) { + type = currentElement.getValue().toString(); + } + if ("enum".equals(currentElementKey)) { + isEnumClass = true; + enums = currentElement.getValue().asList().toString(); + enums = enums.substring(enums.indexOf("[") + 1, enums.indexOf("]")); + } + if ("properties".equals(currentElementKey)) { + handleProperties(props, currentElement.getValue().asMap()); + } + if ("required".equals(currentElementKey)) { + if (required == null) { + required = new ArrayList<>(); + } + required.addAll(currentElement.getValue().asList()); + } + // expend the ref elements and add to the queue + if ("$ref".equals(currentElementKey)) { + String s = currentElement.getValue().toString(); + s = s.substring(s.lastIndexOf('/') + 1); + if (seen.contains(s)) continue; + seen.add(s); + for (Map.Entry schema : schemas.get(s).asMap().entrySet()) { + schemaElementQueue.offer(schema); + } + } + // expand the allOf elements and add to the queue + if ("allOf".equals(currentElementKey)) { + for (Any listItem : currentElement.getValue().asList()) { + for (Map.Entry allOfItem : listItem.asMap().entrySet()) { + schemaElementQueue.offer(allOfItem); + } + } + } + // call loadModel recursively to generate new model corresponding to each oneOf elements + if ("oneOf".equals(currentElementKey)) { + isAbstractClass = true; + parentProps.addAll(props); + String parentName = classVarName.substring(0, 1) + classVarName.substring(1); + for (Any listItem : currentElement.getValue().asList()) { + for (Map.Entry oneOfItem : listItem.asMap().entrySet()) { + if ("$ref".equals(oneOfItem.getKey())) { + String s = oneOfItem.getValue().toString(); + s = s.substring(s.lastIndexOf('/') + 1); + loadModel(extendModelName(s, classVarName), parentName, schemas.get(s).asMap(), schemas, overwriteModel, targetPath, modelPackage, modelCreators, references, parentProps); + } + } + } + } + } + + // Check the type of current schema. Generation will be executed only if the type of the schema equals to object. + // Since generate a model for primitive types and arrays do not make sense, and an error class would be generated + // due to lack of properties if force to generate. + if (type == null) { + throw new RuntimeException("Cannot find the type of \"" + modelFileName + "\" in #/components/schemas/ of the specification file."); + } + + if ("object".equals(type) || isEnumClass) { + if (!overwriteModel && checkExist(targetPath, ("src.main.java." + modelPackage).replace(".", separator), modelFileName + ".java")) { + return; + } + + final String enumsIfClass = isEnumClass ? enums : null; + final boolean abstractIfClass = isAbstractClass; + modelCreators.add(() -> { + final int referencesCount = references.size(); + for (Map properties : props) { + Any any = properties.get("type"); + if (any != null) { + if (any.valueType() == ValueType.STRING) { + Any resolved = references.get(any.toString()); + if (resolved == null) { + continue; + } + any = new UnresolvedTypeHolderAny(resolved); + properties.put("type", any); + } + + int iteration = 0; + do { + UnresolvedTypeAny previous = null; + while (any instanceof UnresolvedTypeAny) { + previous = (UnresolvedTypeAny)any; + any = ((UnresolvedTypeAny)any).get(); + } + + if (any == null) { + break; + } else if (iteration++ > referencesCount) { + throw new TypeNotPresentException(any.toString(), null); + } + + if (any.valueType() == ValueType.STRING) { + any = references.get(any.toString()); + if (any == null) { + break; + } else { + previous.set(any); + } + } else { + break; + } + } while (true); + } + } + + try { + transfer(targetPath, + ("src.main.java." + modelPackage).replace(".", separator), + modelFileName + ".java", + enumsIfClass == null + ? templates.rest.pojo.template(modelPackage, modelFileName, parentClassName, classVarName, abstractIfClass, props, parentClassProps) + : templates.rest.enumClass.template(modelPackage, modelFileName, enumsIfClass)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + } else { + HashMap map = new HashMap<>(1); + map.put(classVarName, Any.wrap(value)); + handleProperties(props, map); + if (props.isEmpty()) { + throw new IllegalStateException("Properties empty for " + classVarName + "!"); + } + + references.put(modelFileName, props.get(0).get("type")); + } + } + private String extendModelName(String str1, String str2) { + return str1 + str2.substring(0, 1).toUpperCase() + str2.substring(1); + } } diff --git a/light-rest-4j/src/main/java/com/networknt/codegen/rest/SwaggerGenerator.java b/light-rest-4j/src/main/java/com/networknt/codegen/rest/SwaggerGenerator.java index 5d4eabac9..aefa8f587 100644 --- a/light-rest-4j/src/main/java/com/networknt/codegen/rest/SwaggerGenerator.java +++ b/light-rest-4j/src/main/java/com/networknt/codegen/rest/SwaggerGenerator.java @@ -259,7 +259,7 @@ public void generate(String targetPath, Object model, Any config) throws IOExcep if(!overwriteModel && checkExist(targetPath, ("src.main.java." + modelPackage).replace(".", separator), modelFileName + ".java")) { continue; } - transfer(targetPath, ("src.main.java." + modelPackage).replace(".", separator), modelFileName + ".java", templates.rest.pojo.template(modelPackage, modelFileName, classVarName, props)); + transfer(targetPath, ("src.main.java." + modelPackage).replace(".", separator), modelFileName + ".java", templates.rest.pojo.template(modelPackage, modelFileName, null, classVarName, false, props, null)); } } diff --git a/light-rest-4j/src/main/resources/templates/rest/pojo.rocker.raw b/light-rest-4j/src/main/resources/templates/rest/pojo.rocker.raw index f8e610512..4cf08b8d5 100644 --- a/light-rest-4j/src/main/resources/templates/rest/pojo.rocker.raw +++ b/light-rest-4j/src/main/resources/templates/rest/pojo.rocker.raw @@ -3,7 +3,7 @@ @import java.util.List @option discardLogicWhitespace=true -@args (String modelPackage, String className, String classVarName, List> props) +@args (String modelPackage, String className, String parentClassName, String classVarName, boolean isAbstractClass, List> props, List> parentProps) package @modelPackage; import java.util.Arrays; @@ -11,7 +11,7 @@ import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; -public class @className { +public@if(isAbstractClass){ abstract} class @className @if(parentClassName != null) {extends @parentClassName} { @for (prop: props) { @if(prop.get("isEnum").toBoolean()) {@templates.rest.enumInline.template(prop)} else {private @prop.get("type") @prop.get("name");} @@ -57,12 +57,14 @@ public class @className { @className @classVarName = (@className) o; return @for ((i, prop): props) {@if (i.index() < props.size() - 1) {@prop.get("comparator")?:"Objects".equals(@prop.get("name"), @classVarName.@prop.get("name")) && - } else {@prop.get("comparator")?:"Objects".equals(@prop.get("name"), @classVarName.@prop.get("name"))}}; + } else {@prop.get("comparator")?:"Objects".equals(@prop.get("name"), @classVarName.@prop.get("name"))}}@if(parentProps != null && parentProps.size() > 0){ && + @for ((i, prop): parentProps) {@if (i.index() < parentProps.size() - 1) {@prop.get("comparator")?:"Objects".equals(this.@prop.get("getter")(), @classVarName.@prop.get("getter")()) && + } else {@prop.get("comparator")?:"Objects".equals(this.@prop.get("getter")(), @classVarName.@prop.get("getter")())}}}; } @@Override public int hashCode() { - return Objects.hash(@for((i, prop): props) {@with(String hasher = prop.get("hasher"), String NULL = null){@if (i.index() < props.size() - 1) {@if (hasher == null){@prop.get("name")} else {@hasher@NULL?:"".hashCode(@prop.get("name"))}, } else {@if (hasher == null){@prop.get("name")} else {@hasher@NULL?:"".hashCode(@prop.get("name"))}}}}); + return Objects.hash(@for((i, prop): props) {@with(String hasher = prop.get("hasher"), String NULL = null){@if (i.index() < props.size() - 1) {@if (hasher == null){@prop.get("name")} else {@hasher@NULL?:"".hashCode(@prop.get("name"))}, } else {@if (hasher == null){@prop.get("name")} else {@hasher@NULL?:"".hashCode(@prop.get("name"))}}}}@if(parentProps != null && parentProps.size() > 0){, @for((i, prop): parentProps) {@with(String hasher = prop.get("hasher"), String NULL = null){@if (i.index() < parentProps.size() - 1) {@if (hasher == null){this.@prop.get("getter")()} else {@hasher@NULL?:"".hashCode(this.@prop.get("getter")())}, } else {@if (hasher == null){this.@prop.get("getter")()} else {@hasher@NULL?:"".hashCode(this.@prop.get("getter")())}}}}}); } } @@ -71,8 +73,8 @@ public class @className { StringBuilder sb = new StringBuilder(); sb.append("class @className {\n"); @for(prop: props) { - sb.append(" @prop.get("name"): ").append(toIndentedString(@prop.get("name"))).append("\n"); - } + sb.append(" @prop.get("name"): ").append(toIndentedString(@prop.get("name"))).append("\n");}@if(parentProps != null && parentProps.size() > 0){@for(prop: parentProps) { + sb.append(" @prop.get("name"): ").append(toIndentedString(this.@prop.get("getter")())).append("\n");}} sb.append("}"); return sb.toString(); }