Skip to content

Commit

Permalink
improvements to codegen: enums and POST operations
Browse files Browse the repository at this point in the history
  • Loading branch information
EricWittmann committed Aug 15, 2019
1 parent a5ab637 commit b229076
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
/*
* Copyright 2019 Red Hat
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.apicurio.hub.api.codegen;


import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.jsonschema2pojo.rules.PrimitiveTypes.isPrimitive;
import static org.jsonschema2pojo.util.TypeUtil.resolveType;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.exception.ClassAlreadyExistsException;
import org.jsonschema2pojo.exception.GenerationException;
import org.jsonschema2pojo.rules.Rule;
import org.jsonschema2pojo.rules.RuleFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sun.codemodel.ClassType;
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.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;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;

/**
* @author [email protected]
*/
public class JaxRsEnumRule implements Rule<JClassContainer, JType> {

private static final String VALUE_FIELD_NAME = "value";

private final RuleFactory ruleFactory;

protected JaxRsEnumRule(RuleFactory ruleFactory) {
this.ruleFactory = ruleFactory;
}

/**
* Applies this schema rule to take the required code generation steps.
* <p>
* A Java {@link Enum} is created, with constants for each of the enum
* values present in the schema. The enum name is derived from the nodeName,
* and the enum type itself is created as an inner class of the owning type.
* In the rare case that no owning type exists (the enum is the root of the
* schema), then the enum becomes a public class in its own right.
* <p>
* The actual JSON value for each enum constant is held in a property called
* "value" in the generated type. A static factory method
* <code>fromValue(String)</code> is added to the generated enum, and the
* methods are annotated to allow Jackson to marshal/unmarshal values
* correctly.
*
* @param nodeName
* the name of the property which is an "enum"
* @param node
* the enum node
* @param container
* the class container (class or package) to which this enum
* should be added
* @return the newly generated Java type that was created to represent the
* given enum
* @see org.jsonschema2pojo.rules.Rule#apply(java.lang.String, com.fasterxml.jackson.databind.JsonNode, com.fasterxml.jackson.databind.JsonNode, java.lang.Object, org.jsonschema2pojo.Schema)
*/
@Override
public JType apply(String nodeName, JsonNode node, JsonNode parent, JClassContainer container,
Schema schema) {

JDefinedClass _enum;
try {
_enum = createEnum(node, nodeName, container);
} catch (ClassAlreadyExistsException e) {
return e.getExistingClass();
}

schema.setJavaTypeIfEmpty(_enum);

if (node.has("javaInterfaces")) {
addInterfaces(_enum, node.get("javaInterfaces"));
}

// 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, parent, 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;
}

private JDefinedClass createEnum(JsonNode node, String nodeName, JClassContainer container) throws ClassAlreadyExistsException {

int modifiers = container.isPackage() ? JMod.PUBLIC : JMod.PUBLIC;

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

if (isPrimitive(fqn, container.owner())) {
throw new GenerationException("Primitive type '" + fqn + "' cannot be used as an enum.");
}

try {
Class<?> existingClass = Thread.currentThread().getContextClassLoader().loadClass(fqn);
throw new ClassAlreadyExistsException(container.owner().ref(existingClass));
} catch (ClassNotFoundException e) {
return container.owner()._class(fqn, ClassType.ENUM);
}
} else {
try {
return container._class(modifiers, getEnumName(nodeName, node, container), ClassType.ENUM);
} catch (JClassAlreadyExistsException e) {
throw new GenerationException(e);
}
}
} catch (JClassAlreadyExistsException e) {
throw new ClassAlreadyExistsException(e.getExistingClass());
}
}

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(backingType, "value");

JBlock body = fromValue.body();
JVar constant = body.decl(_enum, "constant");
constant.init(quickLookupMap.invoke("get").arg(valueParam));

JConditional _if = body._if(constant.eq(JExpr._null()));

JInvocation illegalArgumentException = JExpr._new(_enum.owner().ref(IllegalArgumentException.class));
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(_enum, fromValue);
}

private JFieldVar addQuickLookupMap(JDefinedClass _enum, JType backingType) {

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(backingType.boxify(), _enum);
lookupMap.init(JExpr._new(lookupImplType));

JForEach forEach = _enum.init().forEach(_enum, "c", JExpr.invoke("values"));
JInvocation put = forEach.body().invoke(lookupMap, "put");
put.arg(forEach.var().ref("value"));
put.arg(forEach.var());

return lookupMap;
}

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(type, VALUE_FIELD_NAME);
JBlock body = constructor.body();
body.assign(JExpr._this().ref(valueField), valueParam);

return valueField;
}

private void addToString(JDefinedClass _enum, JFieldVar valueField) {
JMethod toString = _enum.method(JMod.PUBLIC, String.class, "toString");
JBlock body = toString.body();

JExpression toReturn = JExpr._this().ref(valueField);
if(!isString(valueField.type())){
toReturn = toReturn.plus(JExpr.lit(""));
}

body._return(toReturn);

toString.annotate(Override.class);
}

private void addValueMethod(JDefinedClass _enum, JFieldVar valueField) {
JMethod fromValue = _enum.method(JMod.PUBLIC, valueField.type(), "value");

JBlock body = fromValue.body();
body._return(JExpr._this().ref(valueField));

ruleFactory.getAnnotator().enumValueMethod(_enum, 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<String> existingConstantNames = new ArrayList<String>();
for (int i = 0; i < node.size(); i++) {
JsonNode value = node.path(i);

if (!value.isNull()) {
String constantName = getConstantName(value.asText(), customNames.path(i).asText());
constantName = makeUnique(constantName, existingConstantNames);
existingConstantNames.add(constantName);

JEnumConstant constant = _enum.enumConstant(constantName);

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(_enum, constant, value.asText());
}
}
}

private String getEnumName(String nodeName, JsonNode node, JClassContainer container) {
String fieldName = ruleFactory.getNameHelper().getFieldName(nodeName, node);
String className = ruleFactory.getNameHelper().replaceIllegalCharacters(capitalize(fieldName));
String normalizedName = ruleFactory.getNameHelper().normalizeName(className);

Collection<String> existingClassNames = new ArrayList<String>();
for (Iterator<JDefinedClass> classes = container.classes(); classes.hasNext();) {
existingClassNames.add(classes.next().name());
}
return makeUnique(normalizedName, existingClassNames);
}

private String makeUnique(String name, Collection<String> existingNames) {
boolean found = false;
for (String existingName : existingNames) {
if (name.equalsIgnoreCase(existingName)) {
found = true;
break;
}
}
if (found) {
name = makeUnique(name + "_", existingNames);
}
return name;
}

protected String getConstantName(String nodeName, String customName) {
if (isNotBlank(customName)) {
return customName;
}

if (isEmpty(nodeName)) {
nodeName = "__EMPTY__";
} else if (Character.isDigit(nodeName.charAt(0))) {
nodeName = "_" + nodeName;
}

return nodeName;
}

private void addInterfaces(JDefinedClass jclass, JsonNode javaInterfaces) {
for (JsonNode i : javaInterfaces) {
jclass._implements(resolveType(jclass._package(), i.asText()));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jsonschema2pojo.Annotator;
import org.jsonschema2pojo.DefaultGenerationConfig;
import org.jsonschema2pojo.GenerationConfig;
import org.jsonschema2pojo.Jackson2Annotator;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.SchemaGenerator;
import org.jsonschema2pojo.SchemaMapper;
import org.jsonschema2pojo.SchemaStore;
import org.jsonschema2pojo.rules.Rule;
import org.jsonschema2pojo.rules.RuleFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -54,6 +56,7 @@
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeSpec.Builder;
import com.sun.codemodel.JClassContainer;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JType;

Expand Down Expand Up @@ -363,7 +366,7 @@ private String generateJavaInterface(CodegenJavaInterface _interface) {
for (CodegenJavaArgument cgArgument : cgMethod.getArguments()) {
TypeName defaultParamType = ClassName.OBJECT;
if (cgArgument.getIn().equals("body")) {
defaultParamType = ClassName.get("javax.ws.rs.core", "Request");
defaultParamType = ClassName.get("java.io", "InputStream");
}
TypeName paramType = generateTypeName(cgArgument.getCollection(), cgArgument.getType(),
cgArgument.getFormat(), cgArgument.getRequired(), defaultParamType);
Expand Down Expand Up @@ -546,7 +549,7 @@ public boolean isIncludeToString() {
};

SchemaMapper schemaMapper = new SchemaMapper(
new RuleFactory(config, new Jackson2Annotator(config), new SchemaStore() {
new JaxRsRuleFactory(config, new Jackson2Annotator(config), new SchemaStore() {
@Override
public Schema create(Schema parent, String path, String refFragmentPathDelimiters) {
String beanClassname = schemaRefToFQCN(path);
Expand Down Expand Up @@ -658,6 +661,24 @@ public JaxRsProjectSettings getSettings() {
public void setUpdateOnly(boolean updateOnly) {
this.updateOnly = updateOnly;
}

public static class JaxRsRuleFactory extends RuleFactory {

/**
* Constructor.
*/
public JaxRsRuleFactory(GenerationConfig generationConfig, Annotator annotator, SchemaStore schemaStore) {
super(generationConfig, annotator, schemaStore);
}

/**
* @see org.jsonschema2pojo.rules.RuleFactory#getEnumRule()
*/
@Override
public Rule<JClassContainer, JType> getEnumRule() {
return new JaxRsEnumRule(this);
}
}

/**
* Represents some basic meta information about the project being generated.
Expand Down
Loading

0 comments on commit b229076

Please sign in to comment.