Java tutorial
/** * Copyright (C) 2013 Steve Owens (DoItNext.com) http://www.doitnext.com * * 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 com.doitnext.jsonschema.generator; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.lang3.StringUtils; import com.doitnext.jsonschema.annotations.JsonSchemaClass; import com.doitnext.jsonschema.annotations.JsonSchemaProperty; /** * Generates a JSON Schema from an annotated class. * * @author Steve Owens (steve@doitnext.com) * */ public class SchemaGen { private SchemaGen() { // Hidden constructor. This is a static class. } /** * Converts a class defintiion to a JSON Schema definition. * * @param classz the annotated class for which to generate a JSON schema. * @param uriPrefix any classes that this class aggregates will be documented in * a separate schema and a $ref will be used for the properties herein. * The URI base is the prefix to apply to the references. * @return the JSON schema as a string. */ public static String toSchema(Class<?> classz, String uriPrefix) { StringBuilder sb = new StringBuilder(); Map<String, String> declarations = new HashMap<String, String>(); toSchema(classz, sb, null, declarations, uriPrefix); return sb.toString(); } private static void toSchema(Class<?> classz, StringBuilder sb, JsonSchemaProperty propertyDescriptor, Map<String, String> declarations, String uriPrefix) { sb.append("{"); if (!handleSimpleType(classz, sb, propertyDescriptor, declarations, uriPrefix)) { if (!handleArrayType(classz, sb, propertyDescriptor, declarations, uriPrefix)) { handleObjectType(classz, sb, propertyDescriptor, declarations, true, uriPrefix); } } sb.append("}"); } private static boolean handleSimpleType(Class<?> classz, StringBuilder sb, JsonSchemaProperty propertyDescriptor, Map<String, String> declarations, String uriPrefix) { boolean result = false; if (classz.equals(String.class)) { sb.append("\"type\":\"string\""); result = true; } else if (classz.equals(Float.class)) { sb.append("\"type\":\"number\""); result = true; } else if (classz.equals(Double.class)) { sb.append("\"type\":\"number\""); result = true; } else if (classz.equals(Integer.class)) { sb.append("\"type\":\"integer\""); result = true; } else if (classz.equals(Long.class)) { sb.append("\"type\":\"long\""); result = true; } else if (classz.equals(Boolean.class)) { sb.append("\"type\":\"boolean\""); result = true; } if (result) { Map<String, Boolean> flags = new HashMap<String, Boolean>(); flags.put("hasTitle", false); flags.put("hasDescription", false); flags.put("hasType", true); flags.put("hasEnum", false); if (handlePropertyDescriptor(sb, propertyDescriptor, flags, ", ", result, uriPrefix)) { result = true; } } return result; } private static boolean handleArrayType(Class<?> classz, StringBuilder sb, JsonSchemaProperty propertyDescriptor, Map<String, String> declarations, String uriPrefix) { boolean result = false; Map<String, Boolean> flags = new HashMap<String, Boolean>(); flags.put("hasTitle", false); flags.put("hasDescription", false); flags.put("hasType", false); flags.put("hasEnum", false); Class<?> itemClass = null; String prepend = ", "; if (classz.isArray()) { sb.append("\"type\":\"array\""); itemClass = classz.getComponentType(); flags.put("hasType", true); result = true; } else if (Collection.class.isAssignableFrom(classz)) { if (propertyDescriptor.collectionContains().equals(JsonSchemaProperty.DEFAULT.class)) throw new IllegalArgumentException(String.format( "The property descriptor is not valid. You must specify collectionContains for collection type properties. %s", propertyDescriptor)); itemClass = propertyDescriptor.collectionContains(); sb.append("\"type\":\"array\""); flags.put("hasType", false); result = true; } if (result == true) { handlePropertyDescriptor(sb, propertyDescriptor, flags, prepend, result, uriPrefix); boolean inline = true; StringBuilder sb2 = new StringBuilder(); sb2.append("{"); if (!handleSimpleType(itemClass, sb2, null, declarations, uriPrefix)) { if (!handleArrayType(itemClass, sb2, propertyDescriptor, declarations, uriPrefix)) { inline = false; handleObjectType(itemClass, sb2, propertyDescriptor, declarations, false, uriPrefix); } } sb2.append("}"); if (inline) { sb.append(prepend); sb.append("\"items\":"); sb.append(sb2.toString()); } else { String id = null; JsonSchemaClass jsc = itemClass.getAnnotation(JsonSchemaClass.class); if (jsc != null) id = jsc.id(); else id = itemClass.getName(); declarations.put(id, sb2.toString()); sb.append(prepend); sb.append("\"items\":{\"$ref\":\"" + uriPrefix); sb.append(id); sb.append("\"}"); } } return result; } private static boolean handlePropertyDescriptor(StringBuilder sb, JsonSchemaProperty propertyDescriptor, Map<String, Boolean> flags, String prepend, boolean result, String uriPrefix) { if (propertyDescriptor != null) { if (result == true) prepend = ", "; if (!flags.get("hasTitle") && !StringUtils.isEmpty(propertyDescriptor.title())) { sb.append(prepend); sb.append("\"title\":\"" + propertyDescriptor.title() + "\""); prepend = ", "; flags.put("hasTitle", true); } if (!flags.get("hasDescription") && !StringUtils.isEmpty(propertyDescriptor.description())) { sb.append(prepend); sb.append("\"description\":\"" + propertyDescriptor.description() + "\""); prepend = ","; flags.put("hasDescription", true); } if (!flags.get("hasEnum") && propertyDescriptor.enumValues().length > 0) { sb.append(prepend); sb.append("\"enum\":["); String prepend2 = ""; for (String value : propertyDescriptor.enumValues()) { sb.append(prepend2); sb.append("\""); sb.append(value); sb.append("\""); prepend2 = ", "; } sb.append("]"); prepend = ", "; flags.put("hasEnum", true); } if (!StringUtils.isEmpty(propertyDescriptor.defaultValue())) { sb.append(prepend); sb.append("\"defaultValue\":\"" + propertyDescriptor.defaultValue() + "\""); prepend = ", "; } if (propertyDescriptor.dependencies().length > 0) { sb.append(prepend); sb.append("\"dependencies\":["); String prepend2 = ""; for (String value : propertyDescriptor.dependencies()) { sb.append(prepend2); sb.append("\""); sb.append(value); sb.append("\""); prepend2 = ", "; } sb.append("]"); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.exclusiveMaximum())) { sb.append(prepend); sb.append("\"exclusiveMaximum\":\"" + propertyDescriptor.exclusiveMaximum() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.exclusiveMinimum())) { sb.append(prepend); sb.append("\"exclusiveMinimum\":\"" + propertyDescriptor.exclusiveMinimum() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.maximum())) { sb.append(prepend); sb.append("\"maximum\":\"" + propertyDescriptor.maximum() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.minimum())) { sb.append(prepend); sb.append("\"minimum\":\"" + propertyDescriptor.minimum() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.maxItems())) { sb.append(prepend); sb.append("\"maxItems\":\"" + propertyDescriptor.maxItems() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.minItems())) { sb.append(prepend); sb.append("\"minItems\":\"" + propertyDescriptor.minItems() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.maxLength())) { sb.append(prepend); sb.append("\"maxLength\":\"" + propertyDescriptor.maxLength() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.minLength())) { sb.append(prepend); sb.append("\"minLength\":\"" + propertyDescriptor.minLength() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.pattern())) { sb.append(prepend); sb.append("\"patern\":\"" + propertyDescriptor.pattern() + "\""); prepend = ", "; } if (!StringUtils.isEmpty(propertyDescriptor.format())) { sb.append(prepend); sb.append("\"format\":\"" + propertyDescriptor.format() + "\""); prepend = ", "; } sb.append(prepend); sb.append("\"required\":" + Boolean.toString(propertyDescriptor.required())); prepend = ", "; result = true; } return result; } private static boolean handleProperties(Class<?> classz, StringBuilder sb, Map<String, String> declarations, String uriPrefix) { boolean result = false; String prepend = ""; Set<String> processedFields = new HashSet<String>(); sb.append("{"); for (Field field : classz.getFields()) { JsonSchemaProperty propertyDecl = field.getAnnotation(JsonSchemaProperty.class); if (propertyDecl != null) { Class<?> propClassz = field.getType(); StringBuilder sb2 = new StringBuilder(); boolean inline = true; sb2.append("{"); if (!handleSimpleType(propClassz, sb2, propertyDecl, declarations, uriPrefix)) { if (!handleArrayType(propClassz, sb2, propertyDecl, declarations, uriPrefix)) { inline = false; } } sb2.append("}"); if (inline) { sb.append(prepend); sb.append("\""); sb.append(propertyDecl.name()); sb.append("\":"); sb.append(sb2.toString()); prepend = ", "; } else { String id = null; JsonSchemaClass jsc = propClassz.getAnnotation(JsonSchemaClass.class); if (jsc != null) id = jsc.id(); else id = propClassz.getName(); declarations.put(id, sb2.toString()); sb.append(prepend); sb.append("\""); sb.append(propertyDecl.name()); sb.append("\":{\"$ref\":\""); if (!StringUtils.isEmpty(uriPrefix)) sb.append(uriPrefix); sb.append(id); sb.append("\"}"); prepend = ", "; } processedFields.add(propertyDecl.name()); } } for (Method method : classz.getMethods()) { JsonSchemaProperty propertyDecl = method.getAnnotation(JsonSchemaProperty.class); if (propertyDecl != null && !processedFields.contains(propertyDecl.name())) { Class<?> propClassz = method.getReturnType(); StringBuilder sb2 = new StringBuilder(); boolean inline = true; sb2.append("{"); if (!handleSimpleType(propClassz, sb2, propertyDecl, declarations, uriPrefix)) { if (!handleArrayType(propClassz, sb2, propertyDecl, declarations, uriPrefix)) { inline = false; } } sb2.append("}"); if (inline) { sb.append(prepend); sb.append("\""); sb.append(propertyDecl.name()); sb.append("\":"); sb.append(sb2.toString()); prepend = ", "; } else { String id = null; JsonSchemaClass jsc = propClassz.getAnnotation(JsonSchemaClass.class); if (jsc != null) id = jsc.id(); else id = propClassz.getName(); declarations.put(id, sb2.toString()); sb.append(prepend); sb.append("\""); sb.append(propertyDecl.name()); sb.append("\":{\"$ref\":\""); if (!StringUtils.isEmpty(uriPrefix)) sb.append(uriPrefix); sb.append(id); sb.append("\"}"); prepend = ", "; } processedFields.add(propertyDecl.name()); } } sb.append("}"); return result; } private static boolean handleObjectType(Class<?> classz, StringBuilder sb, JsonSchemaProperty propertyDescriptor, Map<String, String> declarations, boolean topLevel, String uriPrefix) { boolean result = false; String prepend = ""; Map<String, Boolean> flags = new HashMap<String, Boolean>(); flags.put("hasTitle", false); flags.put("hasDescription", false); flags.put("hasType", false); flags.put("hasEnum", false); if (classz.isEnum()) { sb.append("\"type\":\"string\","); sb.append("\"enum\":["); for (Enum<?> c : (Enum<?>[]) classz.getEnumConstants()) { sb.append("\""); sb.append(c.toString()); sb.append("\", "); } sb.append("]"); flags.put("hasType", true); flags.put("hasEnum", true); result = true; } if (!flags.get("hasType")) { sb.append(prepend); sb.append("\"type\":\"object\""); prepend = ", "; flags.put("hasType", true); result = true; } if (handlePropertyDescriptor(sb, propertyDescriptor, flags, prepend, result, uriPrefix)) { result = true; } JsonSchemaClass annotation = classz.getAnnotation(JsonSchemaClass.class); if (annotation != null) { if (result == true) prepend = ", "; if (!StringUtils.isEmpty(annotation.id())) { sb.append(prepend); sb.append("\"id\":\"" + annotation.id() + "\""); prepend = ", "; } if (!flags.get("hasTitle") && !StringUtils.isEmpty(annotation.title())) { sb.append(prepend); sb.append("\"title\":\"" + annotation.title() + "\""); prepend = ", "; } if (!flags.get("hasDescription") && !StringUtils.isEmpty(annotation.description())) { sb.append(prepend); sb.append("\"description\":\"" + annotation.description() + "\""); prepend = ", "; } result = true; } sb.append(prepend); sb.append("\"properties\":"); handleProperties(classz, sb, declarations, uriPrefix); prepend = ", "; return result; } }