Java tutorial
/* * Copyright (c) 2014, Danilo Reinert (daniloreinert@growbit.com) * * This software is dual-licensed under: * * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any * later version; * - the Apache Software License (ASL) version 2.0. * * The text of both licenses is available under the src/resources/ directory of * this project (under the names LGPL-3.0.txt and ASL-2.0.txt respectively). * * Direct link to the sources: * * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt */ package com.github.reinert.jjschema; import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonManagedReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.reinert.jjschema.exception.TypeException; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.math.BigDecimal; import java.util.*; import java.util.Map.Entry; /** * Generates JSON schema from Java Types * * @author reinert */ public abstract class JsonSchemaGenerator { private static final String TAG_PROPERTIES = "properties"; private static final String TAG_REQUIRED = "required"; private static final String TAG_TYPE = "type"; private static final String TAG_ARRAY = "array"; final ObjectMapper mapper = new ObjectMapper(); boolean autoPutVersion = true; boolean sortProperties = true; boolean processFieldsOnly = false; boolean processAnnotatedOnly = false; private Set<ManagedReference> forwardReferences; private Set<ManagedReference> backReferences; protected JsonSchemaGenerator() { } Set<ManagedReference> getForwardReferences() { if (forwardReferences == null) forwardReferences = new LinkedHashSet<ManagedReference>(); return forwardReferences; } <T> void pushFowardReference(ManagedReference fowardReference) { getForwardReferences().add(fowardReference); } <T> boolean isFowardReferencePiled(ManagedReference fowardReference) { return getForwardReferences().contains(fowardReference); } <T> boolean pullFowardReference(ManagedReference fowardReference) { return getForwardReferences().remove(fowardReference); } Set<ManagedReference> getBackwardReferences() { if (backReferences == null) backReferences = new LinkedHashSet<ManagedReference>(); return backReferences; } <T> void pushBackwardReference(ManagedReference backReference) { getBackwardReferences().add(backReference); } <T> boolean isBackwardReferencePiled(ManagedReference backReference) { return getBackwardReferences().contains(backReference); } <T> boolean pullBackwardReference(ManagedReference backReference) { return getBackwardReferences().remove(backReference); } // void resetProcessedReferences() { // processedReferences = null; // } // // /** // * Reset all utility fields used for generating schemas when asked by the user // */ // void reset() { // resetProcessedReferences(); // } protected ObjectNode createRefSchema(String ref) { return createInstance().put("$ref", ref); } /** * Reads {@link Attributes} annotation and put its values into the * generating schema. Usually, some verification is done for not putting the * default values. * * @param schema * @param props */ protected abstract void processSchemaProperty(ObjectNode schema, Attributes props); protected ObjectNode createInstance() { return mapper.createObjectNode(); } /** * Checks if this generator should put the $schema attribute at the root * schema. * * @return true if it should put the $schema attribute, false otherwise */ public boolean isAutoPutVersion() { return autoPutVersion; } /** * If true, this parameter says that the $schema atribute should be put at * the root of all schemas generated by this SchemaGenerator instace. * * @param autoPutVersion * @return the actual instance of JsonSchemaGenerator */ public JsonSchemaGenerator setAutoPutVersion(boolean autoPutVersion) { this.autoPutVersion = autoPutVersion; return this; } public <T> ObjectNode generateSchema(Class<T> type) throws TypeException { ObjectNode schema = createInstance(); schema = checkAndProcessType(type, schema); return schema; } /** * Checks whether the type is SimpleType (mapped by * {@link SimpleTypeMappings}), Collection or Iterable (for mapping arrays), * Void type (returning null), or custom Class (for mapping objects). * * @param type * @param schema * @return the full schema represented as an ObjectNode. */ protected <T> ObjectNode checkAndProcessType(Class<T> type, ObjectNode schema) throws TypeException { String s = SimpleTypeMappings.forClass(type); // If it is a simple type, then just put the type if (s != null) { schema.put(TAG_TYPE, s); } // If it is a Collection or Iterable the generate the schema as an array else if (Iterable.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type)) { checkAndProcessCollection(type, schema); } // If it is void then return null else if (type == Void.class || type == void.class) { schema = null; } // If it is an Enum than process like enum else if (type.isEnum()) { processEnum(type, schema); } // If none of the above possibilities were true, then it is a custom object else { schema = processCustomType(type, schema); } return schema; } /** * Generates the schema of custom java types * * @param type * @param schema * @return the full schema of custom java types */ protected <T> ObjectNode processCustomType(Class<T> type, ObjectNode schema) throws TypeException { schema.put(TAG_TYPE, "object"); // fill root object properties processRootAttributes(type, schema); if (this.processFieldsOnly) { // Process fields only processFields(type, schema); } else { // Generate the schemas of type's properties processProperties(type, schema); } // Merge the actual type's schema with a parent type's schema (if it exists!) schema = mergeWithParent(type, schema); return schema; } /** * Generates the schema of collections java types * * @param type * @param schema */ private <T> void checkAndProcessCollection(Class<T> type, ObjectNode schema) throws TypeException { // If the type extends from AbstracctCollection, then it is considered // as a simple array type if (AbstractCollection.class.isAssignableFrom(type)) { schema.put(TAG_TYPE, TAG_ARRAY); } // Otherwise it is processed as a custom array type else { processRootAttributes(type, schema); // NOTE: Customized Iterable/Collection Wrapper Class must declare // the intended Collection as the first field processCustomCollection(type, schema); } } private <T> void processCustomCollection(Class<T> type, ObjectNode schema) throws TypeException { schema.put(TAG_TYPE, TAG_ARRAY); Field field = type.getDeclaredFields()[0]; ParameterizedType genericType = (ParameterizedType) field.getGenericType(); Class<?> genericClass = (Class<?>) genericType.getActualTypeArguments()[0]; ObjectNode itemsSchema = generateSchema(genericClass); itemsSchema.remove("$schema"); schema.put("items", itemsSchema); } private <T> void processEnum(Class<T> type, ObjectNode schema) { ArrayNode enumArray = schema.putArray("enum"); for (T constant : type.getEnumConstants()) { String value = constant.toString(); // Check if value is numeric try { // First verifies if it is an integer Long integer = Long.parseLong(value); enumArray.add(integer); } // If not then verifies if it is an floating point number catch (NumberFormatException e) { try { BigDecimal number = new BigDecimal(value); enumArray.add(number); } // Otherwise add as String catch (NumberFormatException e1) { enumArray.add(value); } } } } private void processPropertyCollection(Method method, Field field, ObjectNode schema) throws TypeException { schema.put(TAG_TYPE, TAG_ARRAY); Class<?> genericClass = null; if (method != null) { Type methodType = method.getGenericReturnType(); if (!ParameterizedType.class.isAssignableFrom(methodType.getClass())) { throw new TypeException("Collection property must be parameterized: " + method.getName()); } ParameterizedType genericType = (ParameterizedType) methodType; genericClass = (Class<?>) genericType.getActualTypeArguments()[0]; } else { genericClass = field.getClass(); } schema.put("items", generateSchema(genericClass)); } protected <T> void processRootAttributes(Class<T> type, ObjectNode schema) { Attributes sProp = type.getAnnotation(Attributes.class); if (sProp != null) processSchemaProperty(schema, sProp); } protected <T> void processProperties(Class<T> type, ObjectNode schema) throws TypeException { HashMap<Method, Field> props = findProperties(type); for (Map.Entry<Method, Field> entry : props.entrySet()) { Field field = entry.getValue(); Method method = entry.getKey(); ObjectNode prop = generatePropertySchema(type, method, field); if (prop != null && field != null) { addPropertyToSchema(schema, field, method, prop); } } } protected <T> void processFields(Class<T> type, ObjectNode schema) throws TypeException { List<Field> props = findFields(type); for (Field field : props) { ObjectNode prop = generatePropertySchema(type, null, field); if (prop != null && field != null) { addPropertyToSchema(schema, field, null, prop); } } } protected <T> ObjectNode generatePropertySchema(Class<T> type, Method method, Field field) throws TypeException { Class<?> returnType = method != null ? method.getReturnType() : field.getType(); AccessibleObject propertyReflection = field != null ? field : method; SchemaIgnore ignoreAnn = propertyReflection.getAnnotation(SchemaIgnore.class); if (ignoreAnn != null) return null; ObjectNode schema = createInstance(); JsonManagedReference refAnn = propertyReflection.getAnnotation(JsonManagedReference.class); if (refAnn != null) { ManagedReference fowardReference; Class<?> genericClass; Class<?> collectionClass; if (Collection.class.isAssignableFrom(returnType)) { if (method != null) { ParameterizedType genericType = (ParameterizedType) method.getGenericReturnType(); genericClass = (Class<?>) genericType.getActualTypeArguments()[0]; } else { genericClass = field.getClass(); } collectionClass = returnType; } else { genericClass = returnType; } fowardReference = new ManagedReference(type, refAnn.value(), genericClass); if (!isFowardReferencePiled(fowardReference)) { pushFowardReference(fowardReference); } else // if (isBackwardReferencePiled(fowardReference)) { boolean a = pullFowardReference(fowardReference); boolean b = pullBackwardReference(fowardReference); //return null; return createRefSchema("#"); } } JsonBackReference backRefAnn = propertyReflection.getAnnotation(JsonBackReference.class); if (backRefAnn != null) { ManagedReference backReference; Class<?> genericClass; Class<?> collectionClass; if (Collection.class.isAssignableFrom(returnType)) { ParameterizedType genericType = (ParameterizedType) method.getGenericReturnType(); genericClass = (Class<?>) genericType.getActualTypeArguments()[0]; collectionClass = returnType; } else { genericClass = returnType; } backReference = new ManagedReference(genericClass, backRefAnn.value(), type); if (isFowardReferencePiled(backReference) && !isBackwardReferencePiled(backReference)) { pushBackwardReference(backReference); } else { // pullFowardReference(backReference); // pullBackwardReference(backReference); return null; } } if (Collection.class.isAssignableFrom(returnType)) { processPropertyCollection(method, field, schema); } else { schema = generateSchema(returnType); } // Check the field annotations, if the get method references a field, or the // method annotations on the other hand, and processSchemaProperty them to // the JsonSchema object Attributes attrs = propertyReflection.getAnnotation(Attributes.class); if (attrs != null) { processSchemaProperty(schema, attrs); // The declaration of $schema is only necessary at the root object schema.remove("$schema"); } // Check if the Nullable annotation is present, and if so, add 'null' to type attr Nullable nullable = propertyReflection.getAnnotation(Nullable.class); if (nullable != null) { if (returnType.isEnum()) { ((ArrayNode) schema.get("enum")).add("null"); } else { String oldType = schema.get(TAG_TYPE).asText(); ArrayNode typeArray = schema.putArray(TAG_TYPE); typeArray.add(oldType); typeArray.add("null"); } } return schema; } private void addPropertyToSchema(ObjectNode schema, Field field, Method method, ObjectNode prop) { String name = getPropertyName(field, method); if (prop.has("selfRequired")) { ArrayNode requiredNode; if (!schema.has(TAG_REQUIRED)) { requiredNode = schema.putArray(TAG_REQUIRED); } else { requiredNode = (ArrayNode) schema.get(TAG_REQUIRED); } requiredNode.add(name); prop.remove("selfRequired"); } if (!schema.has(TAG_PROPERTIES)) schema.putObject(TAG_PROPERTIES); ((ObjectNode) schema.get(TAG_PROPERTIES)).put(name, prop); } private String getPropertyName(Field field, Method method) { return (field == null) ? firstToLowCase(method.getName().replace("get", "")) : field.getName(); } /** * If the Java Type inherits from other Java Type but Object, then it is * assumed to inherit from other custom type. In this case, the parent class * is processed as well and merged with the child class, having the child a * high priority when both have same attributes filled. * * @param type * @param schema * @return The actual schema merged with its parent schema (if it exists) */ protected <T> ObjectNode mergeWithParent(Class<T> type, ObjectNode schema) throws TypeException { Class<? super T> superclass = type.getSuperclass(); if (superclass != null && superclass != Object.class) { ObjectNode parentSchema = generateSchema(superclass); schema = mergeSchema(parentSchema, schema, false); } return schema; } /** * Merges two schemas. * * @param parent A parent schema considering inheritance * @param child A child schema considering inheritance * @param overwriteChildProperties A boolean to check whether properties (from parent or child) must have higher priority * @return The tow schemas merged */ protected ObjectNode mergeSchema(ObjectNode parent, ObjectNode child, boolean overwriteChildProperties) { Iterator<String> namesIterator = child.fieldNames(); if (overwriteChildProperties) { while (namesIterator.hasNext()) { String propertyName = namesIterator.next(); overwriteProperty(parent, child, propertyName); } } else { while (namesIterator.hasNext()) { String propertyName = namesIterator.next(); if (!TAG_PROPERTIES.equals(propertyName)) { overwriteProperty(parent, child, propertyName); } } ObjectNode properties = (ObjectNode) child.get(TAG_PROPERTIES); if (properties != null) { if (parent.get(TAG_PROPERTIES) == null) { parent.putObject(TAG_PROPERTIES); } Iterator<Entry<String, JsonNode>> it = properties.fields(); while (it.hasNext()) { Entry<String, JsonNode> entry = it.next(); String pName = entry.getKey(); ObjectNode pSchema = (ObjectNode) entry.getValue(); ObjectNode actualSchema = (ObjectNode) parent.get(TAG_PROPERTIES).get(pName); if (actualSchema != null) { mergeSchema(pSchema, actualSchema, false); } ((ObjectNode) parent.get(TAG_PROPERTIES)).put(pName, pSchema); } } } return parent; } protected void overwriteProperty(ObjectNode parent, ObjectNode child, String propertyName) { if (child.has(propertyName)) { parent.put(propertyName, child.get(propertyName)); } } /** * Utility method to find properties from a Java Type following Beans Convention. * * @param type * @return */ private <T> HashMap<Method, Field> findProperties(Class<T> type) { Field[] fields = type.getDeclaredFields(); Method[] methods = type.getMethods(); if (this.sortProperties) { // Ordering the properties Arrays.sort(methods, new Comparator<Method>() { public int compare(Method m1, Method m2) { return m1.getName().compareTo(m2.getName()); } }); } LinkedHashMap<Method, Field> props = new LinkedHashMap<Method, Field>(); // get valid properties (get method and respective field (if exists)) for (Method method : methods) { Class<?> declaringClass = method.getDeclaringClass(); if (declaringClass.equals(Object.class) || Collection.class.isAssignableFrom(declaringClass)) { continue; } if (isGetter(method)) { boolean hasField = false; for (Field field : fields) { String name = getNameFromGetter(method); Attributes attribs = field.getAnnotation(Attributes.class); boolean process = true; if (this.processAnnotatedOnly && attribs == null) { process = false; } if (process && field.getName().equalsIgnoreCase(name)) { props.put(method, field); hasField = true; break; } } if (!hasField) { props.put(method, null); } } } return props; } private <T> List<Field> findFields(Class<T> type) { Field[] fields = type.getDeclaredFields(); if (this.sortProperties) { // Order the fields Arrays.sort(fields, new Comparator<Field>() { public int compare(Field m1, Field m2) { return m1.getName().compareTo(m2.getName()); } }); } List<Field> props = new ArrayList<Field>(); // get fields for (Field field : fields) { Class<?> declaringClass = field.getDeclaringClass(); if (declaringClass.equals(Object.class) || Collection.class.isAssignableFrom(declaringClass)) { continue; } String name = field.getName(); if (field.getName().equalsIgnoreCase(name)) { Attributes attrs = field.getAnnotation(Attributes.class); // Only process annotated fields if processAnnotatedOnly set if (attrs != null || !this.processAnnotatedOnly) { props.add(field); } } } return props; } private String firstToLowCase(String string) { return Character.toLowerCase(string.charAt(0)) + (string.length() > 1 ? string.substring(1) : ""); } private boolean isGetter(final Method method) { return method.getName().startsWith("get") || method.getName().startsWith("is"); } private String getNameFromGetter(final Method getter) { String[] getterPrefixes = { "get", "is" }; String methodName = getter.getName(); String fieldName = null; for (String prefix : getterPrefixes) { if (methodName.startsWith(prefix)) { fieldName = methodName.substring(prefix.length()); } } if (fieldName == null) { return null; } fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1); return fieldName; } }