/**
* Copyright © 2010-2014 Nokia
*
* 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 org.jsonschema2pojo.rules;
import static org.apache.commons.lang3.StringUtils.*;
import static org.jsonschema2pojo.rules.PrimitiveTypes.*;
import static org.jsonschema2pojo.util.TypeUtil.*;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jsonschema2pojo.AnnotationStyle;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.exception.ClassAlreadyExistsException;
import org.jsonschema2pojo.util.NameHelper;
import org.jsonschema2pojo.util.ParcelableHelper;
import org.jsonschema2pojo.util.SerializableHelper;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.ClassType;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JArray;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;
import android.os.Parcelable;
/**
* Applies the generation steps required for schemas of type "object".
*
* @see <a href=
* "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> {
private final RuleFactory ruleFactory;
private final ParcelableHelper parcelableHelper;
protected ObjectRule(RuleFactory ruleFactory, ParcelableHelper parcelableHelper) {
this.ruleFactory = ruleFactory;
this.parcelableHelper = parcelableHelper;
}
/**
* Applies this schema rule to take the required code generation steps.
* <p>
* When this rule is applied for schemas of type object, the properties of
* the schema are used to generate a new Java class and determine its
* characteristics. See other implementers of {@link Rule} for details.
*/
@Override
public JType apply(String nodeName, JsonNode node, JPackage _package, Schema schema) {
JType superType = getSuperType(nodeName, node, _package, schema);
if (superType.isPrimitive() || isFinal(superType)) {
return superType;
}
JDefinedClass jclass;
try {
jclass = createClass(nodeName, node, _package);
} catch (ClassAlreadyExistsException e) {
return e.getExistingClass();
}
jclass._extends((JClass) superType);
schema.setJavaTypeIfEmpty(jclass);
if (node.has("deserializationClassProperty")) {
addJsonTypeInfoAnnotation(jclass, node);
}
if (node.has("title")) {
ruleFactory.getTitleRule().apply(nodeName, node.get("title"), jclass, schema);
}
if (node.has("description")) {
ruleFactory.getDescriptionRule().apply(nodeName, node.get("description"), jclass, schema);
}
ruleFactory.getPropertiesRule().apply(nodeName, node.get("properties"), jclass, schema);
if (ruleFactory.getGenerationConfig().isIncludeToString()) {
addToString(jclass);
}
if (node.has("javaInterfaces")) {
addInterfaces(jclass, node.get("javaInterfaces"));
}
ruleFactory.getAdditionalPropertiesRule().apply(nodeName, node.get("additionalProperties"), jclass, schema);
ruleFactory.getDynamicPropertiesRule().apply(nodeName, node.get("properties"), jclass, schema);
if (node.has("required")) {
ruleFactory.getRequiredArrayRule().apply(nodeName, node.get("required"), jclass, schema);
}
if (ruleFactory.getGenerationConfig().isIncludeHashcodeAndEquals()) {
addHashCode(jclass);
addEquals(jclass);
}
if (ruleFactory.getGenerationConfig().isParcelable()) {
addParcelSupport(jclass);
}
if (ruleFactory.getGenerationConfig().isIncludeConstructors()) {
addConstructors(jclass, node, schema, ruleFactory.getGenerationConfig().isConstructorsRequiredPropertiesOnly());
}
if (ruleFactory.getGenerationConfig().isSerializable()) {
SerializableHelper.addSerializableSupport(jclass);
}
return jclass;
}
private void addParcelSupport(JDefinedClass jclass) {
jclass._implements(Parcelable.class);
parcelableHelper.addWriteToParcel(jclass);
parcelableHelper.addDescribeContents(jclass);
parcelableHelper.addCreator(jclass);
}
/**
* Retrieve the list of properties to go in the constructor from node. This
* is all properties listed in node["properties"] if ! onlyRequired, and
* only required properties if onlyRequired.
*
* @param node
* @return
*/
private LinkedHashSet<String> getConstructorProperties(JsonNode node, boolean onlyRequired) {
if (!node.has("properties")) {
return new LinkedHashSet<String>();
}
LinkedHashSet<String> rtn = new LinkedHashSet<String>();
Set<String> draft4RequiredProperties = new HashSet<String>();
// setup the set of required properties for draft4 style "required"
if (onlyRequired && node.has("required")) {
JsonNode requiredArray = node.get("required");
if (requiredArray.isArray()) {
for (JsonNode requiredEntry: requiredArray) {
if (requiredEntry.isTextual()) {
draft4RequiredProperties.add(requiredEntry.asText());
}
}
}
}
NameHelper nameHelper = ruleFactory.getNameHelper();
for (Iterator<Map.Entry<String, JsonNode>> properties = node.get("properties").fields(); properties.hasNext();) {
Map.Entry<String, JsonNode> property = properties.next();
JsonNode propertyObj = property.getValue();
if (onlyRequired) {
// draft3 style
if (propertyObj.has("required") && propertyObj.get("required").asBoolean()) {
rtn.add(nameHelper.getPropertyName(property.getKey(), property.getValue()));
}
// draft4 style
if (draft4RequiredProperties.contains(property.getKey())) {
rtn.add(nameHelper.getPropertyName(property.getKey(), property.getValue()));
}
} else {
rtn.add(nameHelper.getPropertyName(property.getKey(), property.getValue()));
}
}
return rtn;
}
/**
* Recursive, walks the schema tree and assembles a list of all properties of this schema's super schemas
*/
private LinkedHashSet<String> getSuperTypeConstructorPropertiesRecursive(JsonNode node, Schema schema, boolean onlyRequired) {
Schema superTypeSchema = getSuperSchema(node, schema, true);
if (superTypeSchema == null) {
return new LinkedHashSet<String>();
}
JsonNode superSchemaNode = superTypeSchema.getContent();
LinkedHashSet<String> rtn = getConstructorProperties(superSchemaNode, onlyRequired);
rtn.addAll(getSuperTypeConstructorPropertiesRecursive(superSchemaNode, superTypeSchema, onlyRequired));
return rtn;
}
/**
* Creates a new Java class that will be generated.
*
* @param nodeName
* the node name which may be used to dictate the new class name
* @param node
* the node representing the schema that caused the need for a
* 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
* @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 {
JDefinedClass newType;
try {
boolean usePolymorphicDeserialization = usesPolymorphicDeserialization(node);
if (node.has("javaType")) {
String fqn = substringBefore(node.get("javaType").asText(), "<");
if (isPrimitive(fqn, _package.owner())) {
throw new ClassAlreadyExistsException(primitiveType(fqn, _package.owner()));
}
JClass existingClass;
try {
_package.owner().ref(Thread.currentThread().getContextClassLoader().loadClass(fqn));
existingClass = resolveType(_package, fqn + (node.get("javaType").asText().contains("<") ? "<" + substringAfter(node.get("javaType").asText(), "<") : ""));
throw new ClassAlreadyExistsException(existingClass);
} catch (ClassNotFoundException e) {
}
int index = fqn.lastIndexOf(".") + 1;
if (index >= 0 && index < fqn.length()) {
fqn = fqn.substring(0, index) + ruleFactory.getGenerationConfig().getClassNamePrefix() + fqn.substring(index) + ruleFactory.getGenerationConfig().getClassNameSuffix();
}
try {
_package.owner().ref(Thread.currentThread().getContextClassLoader().loadClass(fqn));
existingClass = resolveType(_package, fqn + (node.get("javaType").asText().contains("<") ? "<" + substringAfter(node.get("javaType").asText(), "<") : ""));
throw new ClassAlreadyExistsException(existingClass);
} catch (ClassNotFoundException e) {
}
if (usePolymorphicDeserialization) {
newType = _package.owner()._class(JMod.PUBLIC, fqn, ClassType.CLASS);
} else {
newType = _package.owner()._class(fqn);
}
} else {
if (usePolymorphicDeserialization) {
newType = _package._class(JMod.PUBLIC, getClassName(nodeName, node, _package), ClassType.CLASS);
} else {
newType = _package._class(getClassName(nodeName, node, _package));
}
}
} catch (JClassAlreadyExistsException e) {
throw new ClassAlreadyExistsException(e.getExistingClass());
}
ruleFactory.getAnnotator().propertyInclusion(newType, node);
return newType;
}
private boolean isFinal(JType superType) {
try {
Class<?> javaClass = Class.forName(superType.fullName());
return Modifier.isFinal(javaClass.getModifiers());
} catch (ClassNotFoundException e) {
return false;
}
}
private JType getSuperType(String nodeName, JsonNode node, JPackage jPackage, Schema schema) {
if (node.has("extends") && node.has("extendsJavaClass")) {
throw new IllegalStateException("'extends' and 'extendsJavaClass' defined simultaneously");
}
JType superType = jPackage.owner().ref(Object.class);
Schema superTypeSchema = getSuperSchema(node, schema, false);
if (superTypeSchema != null) {
superType = ruleFactory.getSchemaRule().apply(nodeName + "Parent", node.get("extends"), jPackage, superTypeSchema);
} else if (node.has("extendsJavaClass")) {
superType = resolveType(jPackage, node.get("extendsJavaClass").asText());
}
return superType;
}
private Schema getSuperSchema(JsonNode node, Schema schema, boolean followRefs) {
if (node.has("extends")) {
String path;
if (schema.getId().getFragment() == null) {
path = "#extends";
} else {
path = "#" + schema.getId().getFragment() + "/extends";
}
Schema superSchema = ruleFactory.getSchemaStore().create(schema, path, ruleFactory.getGenerationConfig().getRefFragmentPathDelimiters());
if (followRefs) {
superSchema = resolveSchemaRefsRecursive(superSchema);
}
return superSchema;
}
return null;
}
private Schema resolveSchemaRefsRecursive(Schema schema) {
JsonNode schemaNode = schema.getContent();
if (schemaNode.has("$ref")) {
schema = ruleFactory.getSchemaStore().create(schema, schemaNode.get("$ref").asText(), ruleFactory.getGenerationConfig().getRefFragmentPathDelimiters());
return resolveSchemaRefsRecursive(schema);
}
return schema;
}
private void addJsonTypeInfoAnnotation(JDefinedClass jclass, JsonNode node) {
if (ruleFactory.getGenerationConfig().getAnnotationStyle() == AnnotationStyle.JACKSON2) {
String annotationName = node.get("deserializationClassProperty").asText();
JAnnotationUse jsonTypeInfo = jclass.annotate(JsonTypeInfo.class);
jsonTypeInfo.param("use", JsonTypeInfo.Id.CLASS);
jsonTypeInfo.param("include", JsonTypeInfo.As.PROPERTY);
jsonTypeInfo.param("property", annotationName);
}
}
private void addToString(JDefinedClass jclass) {
JMethod toString = jclass.method(JMod.PUBLIC, String.class, "toString");
JBlock body = toString.body();
if ( ruleFactory.getGenerationConfig().getToStringExcludes().length > 0 ) {
Class<?> reflectionToStringBuilder = ruleFactory.getGenerationConfig().isUseCommonsLang3() ? org.apache.commons.lang3.builder.ReflectionToStringBuilder.class : org.apache.commons.lang.builder.ReflectionToStringBuilder.class;
JInvocation toStringExclude = jclass.owner().ref(reflectionToStringBuilder).staticInvoke("toStringExclude");
JArray arr = JExpr.newArray(jclass.owner().ref(String.class));
for ( String exclude : ruleFactory.getGenerationConfig().getToStringExcludes() ) {
arr.add(JExpr.lit(exclude));
}
toStringExclude.arg(JExpr._this()).arg(arr);
body._return(toStringExclude);
} else {
Class<?> toStringBuilder = ruleFactory.getGenerationConfig().isUseCommonsLang3() ? org.apache.commons.lang3.builder.ToStringBuilder.class : org.apache.commons.lang.builder.ToStringBuilder.class;
JInvocation reflectionToString = jclass.owner().ref(toStringBuilder).staticInvoke("reflectionToString");
reflectionToString.arg(JExpr._this());
body._return(reflectionToString);
}
toString.annotate(Override.class);
}
private void addHashCode(JDefinedClass jclass) {
Map<String, JFieldVar> fields = jclass.fields();
JMethod hashCode = jclass.method(JMod.PUBLIC, int.class, "hashCode");
Class<?> hashCodeBuilder = ruleFactory.getGenerationConfig().isUseCommonsLang3() ? org.apache.commons.lang3.builder.HashCodeBuilder.class : org.apache.commons.lang.builder.HashCodeBuilder.class;
JBlock body = hashCode.body();
JClass hashCodeBuilderClass = jclass.owner().ref(hashCodeBuilder);
JInvocation hashCodeBuilderInvocation = JExpr._new(hashCodeBuilderClass);
if (!jclass._extends().fullName().equals(Object.class.getName())) {
hashCodeBuilderInvocation = hashCodeBuilderInvocation.invoke("appendSuper").arg(JExpr._super().invoke("hashCode"));
}
for (JFieldVar fieldVar : fields.values()) {
if ((fieldVar.mods().getValue() & JMod.STATIC) == JMod.STATIC) {
continue;
}
hashCodeBuilderInvocation = hashCodeBuilderInvocation.invoke("append").arg(fieldVar);
}
body._return(hashCodeBuilderInvocation.invoke("toHashCode"));
hashCode.annotate(Override.class);
}
private void addConstructors(JDefinedClass jclass, JsonNode node, Schema schema, boolean onlyRequired) {
LinkedHashSet<String> classProperties = getConstructorProperties(node, onlyRequired);
LinkedHashSet<String> combinedSuperProperties = getSuperTypeConstructorPropertiesRecursive(node, schema, onlyRequired);
// no properties to put in the constructor => default constructor is good enough.
if (classProperties.isEmpty() && combinedSuperProperties.isEmpty()) {
return;
}
// add a no-args constructor for serialization purposes
JMethod noargsConstructor = jclass.constructor(JMod.PUBLIC);
noargsConstructor.javadoc().add("No args constructor for use in serialization");
// add the public constructor with property parameters
JMethod fieldsConstructor = jclass.constructor(JMod.PUBLIC);
JBlock constructorBody = fieldsConstructor.body();
JInvocation superInvocation = constructorBody.invoke("super");
Map<String, JFieldVar> fields = jclass.fields();
Map<String, JVar> classFieldParams = new HashMap<String, JVar>();
for (String property : classProperties) {
JFieldVar field = fields.get(property);
if (field == null) {
throw new IllegalStateException("Property " + property + " hasn't been added to JDefinedClass before calling addConstructors");
}
fieldsConstructor.javadoc().addParam(property);
JVar param = fieldsConstructor.param(field.type(), field.name());
constructorBody.assign(JExpr._this().ref(field), param);
classFieldParams.put(property, param);
}
List<JVar> superConstructorParams = new ArrayList<JVar>();
for (String property : combinedSuperProperties) {
JFieldVar field = searchSuperClassesForField(property, jclass);
if (field == null) {
throw new IllegalStateException("Property " + property + " hasn't been added to JDefinedClass before calling addConstructors");
}
JVar param = classFieldParams.get(property);
if (param == null) {
param = fieldsConstructor.param(field.type(), field.name());
}
fieldsConstructor.javadoc().addParam(property);
superConstructorParams.add(param);
}
for (JVar param : superConstructorParams) {
superInvocation.arg(param);
}
}
private static JDefinedClass definedClassOrNullFromType(JType type)
{
if (type == null || type.isPrimitive())
{
return null;
}
JClass fieldClass = type.boxify();
JPackage jPackage = fieldClass._package();
return jPackage._getClass(fieldClass.name());
}
/**
* This is recursive with searchClassAndSuperClassesForField
*/
private JFieldVar searchSuperClassesForField(String property, JDefinedClass jclass) {
JClass superClass = jclass._extends();
JDefinedClass definedSuperClass = definedClassOrNullFromType(superClass);
if (definedSuperClass == null) {
return null;
}
return searchClassAndSuperClassesForField(property, definedSuperClass);
}
private JFieldVar searchClassAndSuperClassesForField(String property, JDefinedClass jclass) {
Map<String, JFieldVar> fields = jclass.fields();
JFieldVar field = fields.get(property);
if (field == null) {
return searchSuperClassesForField(property, jclass);
}
return field;
}
private void addEquals(JDefinedClass jclass) {
Map<String, JFieldVar> fields = jclass.fields();
JMethod equals = jclass.method(JMod.PUBLIC, boolean.class, "equals");
JVar otherObject = equals.param(Object.class, "other");
Class<?> equalsBuilder = ruleFactory.getGenerationConfig().isUseCommonsLang3() ? org.apache.commons.lang3.builder.EqualsBuilder.class : org.apache.commons.lang.builder.EqualsBuilder.class;
JBlock body = equals.body();
body._if(otherObject.eq(JExpr._this()))._then()._return(JExpr.TRUE);
body._if(otherObject._instanceof(jclass).eq(JExpr.FALSE))._then()._return(JExpr.FALSE);
JVar rhsVar = body.decl(jclass, "rhs").init(JExpr.cast(jclass, otherObject));
JClass equalsBuilderClass = jclass.owner().ref(equalsBuilder);
JInvocation equalsBuilderInvocation = JExpr._new(equalsBuilderClass);
if (!jclass._extends().fullName().equals(Object.class.getName())) {
equalsBuilderInvocation = equalsBuilderInvocation.invoke("appendSuper").arg(JExpr._super().invoke("equals").arg(otherObject));
}
for (JFieldVar fieldVar : fields.values()) {
if ((fieldVar.mods().getValue() & JMod.STATIC) == JMod.STATIC) {
continue;
}
equalsBuilderInvocation = equalsBuilderInvocation.invoke("append")
.arg(fieldVar)
.arg(rhsVar.ref(fieldVar.name()));
}
JInvocation reflectionEquals = jclass.owner().ref(equalsBuilder).staticInvoke("reflectionEquals");
reflectionEquals.arg(JExpr._this());
reflectionEquals.arg(otherObject);
body._return(equalsBuilderInvocation.invoke("isEquals"));
equals.annotate(Override.class);
}
private void addInterfaces(JDefinedClass jclass, JsonNode javaInterfaces) {
for (JsonNode i : javaInterfaces) {
jclass._implements(resolveType(jclass._package(), i.asText()));
}
}
private String getClassName(String nodeName, JsonNode node, JPackage _package) {
String prefix = ruleFactory.getGenerationConfig().getClassNamePrefix();
String suffix = ruleFactory.getGenerationConfig().getClassNameSuffix();
String fieldName = ruleFactory.getNameHelper().getFieldName(nodeName, node);
String capitalizedFieldName = capitalize(fieldName);
String fullFieldName = createFullFieldName(capitalizedFieldName, prefix, suffix);
String className = ruleFactory.getNameHelper().replaceIllegalCharacters(fullFieldName);
String normalizedName = ruleFactory.getNameHelper().normalizeName(className);
return makeUnique(normalizedName, _package);
}
private String createFullFieldName(String nodeName, String prefix, String suffix) {
String returnString = nodeName;
if (prefix != null) {
returnString = prefix + returnString;
}
if (suffix != null) {
returnString = returnString + suffix;
}
return returnString;
}
private String makeUnique(String className, JPackage _package) {
try {
JDefinedClass _class = _package._class(className);
_package.remove(_class);
return className;
} catch (JClassAlreadyExistsException e) {
return makeUnique(className + "_", _package);
}
}
private boolean usesPolymorphicDeserialization(JsonNode node) {
if (ruleFactory.getGenerationConfig().getAnnotationStyle() == AnnotationStyle.JACKSON2) {
return node.has("deserializationClassProperty");
}
return false;
}
}