package org.codehaus.modello.plugin.jsonschema; /* * Copyright (c) 2013, Codehaus.org * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import java.io.File; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Properties; import org.codehaus.modello.ModelloException; import org.codehaus.modello.ModelloParameterConstants; import org.codehaus.modello.model.Model; import org.codehaus.modello.model.ModelAssociation; import org.codehaus.modello.model.ModelClass; import org.codehaus.modello.model.ModelDefault; import org.codehaus.modello.model.ModelField; import org.codehaus.modello.plugins.xml.AbstractXmlJavaGenerator; import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata; import org.codehaus.plexus.util.StringUtils; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator.Feature; /** * @author <a href="mailto:simonetripodi@apache.org">Simone Tripodi</a> * @since 1.8 */ public final class JsonSchemaGenerator extends AbstractXmlJavaGenerator { public void generate( Model model, Properties parameters ) throws ModelloException { initialize( model, parameters ); try { generateJsonSchema( parameters ); } catch ( IOException ioe ) { throw new ModelloException( "Exception while generating JSON Schema.", ioe ); } } private void generateJsonSchema( Properties parameters ) throws IOException, ModelloException { Model objectModel = getModel(); File directory = getOutputDirectory(); if ( isPackageWithVersion() ) { directory = new File( directory, getGeneratedVersion().toString() ); } if ( !directory.exists() ) { directory.mkdirs(); } // we assume parameters not null String schemaFileName = parameters.getProperty( ModelloParameterConstants.OUTPUT_JSONSCHEMA_FILE_NAME ); File schemaFile; if ( schemaFileName != null ) { schemaFile = new File( directory, schemaFileName ); } else { schemaFile = new File( directory, objectModel.getId() + "-" + getGeneratedVersion() + ".schema.json" ); } JsonGenerator generator = new JsonFactory() .enable( Feature.AUTO_CLOSE_JSON_CONTENT ) .enable( Feature.AUTO_CLOSE_TARGET ) .enable( Feature.ESCAPE_NON_ASCII ) .enable( Feature.FLUSH_PASSED_TO_STREAM ) .enable( Feature.QUOTE_FIELD_NAMES ) .enable( Feature.QUOTE_NON_NUMERIC_NUMBERS ) .disable( Feature.WRITE_NUMBERS_AS_STRINGS ) .createGenerator( schemaFile, JsonEncoding.UTF8 ); generator.useDefaultPrettyPrinter(); ModelClass root = objectModel.getClass( objectModel.getRoot( getGeneratedVersion() ), getGeneratedVersion() ); try { generator.writeStartObject(); generator.writeStringField( "$schema", "http://json-schema.org/draft-04/schema#" ); writeClassDocumentation( generator, root, true ); generator.writeObjectFieldStart( "definitions" ); for ( ModelClass current : objectModel.getClasses( getGeneratedVersion() ) ) { if ( !root.equals( current ) ) { writeClassDocumentation( generator, current, false ); } } // end "definitions" generator.writeEndObject(); // end main object generator.writeEndObject(); } finally { generator.close(); } } private void writeClassDocumentation( JsonGenerator generator, ModelClass modelClass, boolean isRoot ) throws IOException { if ( !isRoot ) { generator.writeObjectFieldStart( modelClass.getName() ); } generator.writeStringField( "id", modelClass.getName() + '#' ); writeDescriptionField( generator, modelClass.getDescription() ); writeTypeField( generator, "object" ); generator.writeObjectFieldStart( "properties" ); List<String> required = new LinkedList<String>(); ModelClass reference = modelClass; // traverse the whole modelClass hierarchy to create the nested Builder instance while ( reference != null ) { // collect parameters and set them in the instance object for ( ModelField modelField : reference.getFields( getGeneratedVersion() ) ) { if ( modelField.isRequired() ) { required.add( modelField.getName() ); } // each field is represented as object generator.writeObjectFieldStart( modelField.getName() ); writeDescriptionField( generator, modelField.getDescription() ); if ( modelField instanceof ModelAssociation ) { ModelAssociation modelAssociation = (ModelAssociation) modelField; if ( modelAssociation.isOneMultiplicity() ) { writeTypeField( generator, modelAssociation.getType() ); } else { // MANY_MULTIPLICITY writeTypeField( generator, "array" ); generator.writeObjectFieldStart( "items" ); String type = modelAssociation.getType(); String toType = modelAssociation.getTo(); if ( ModelDefault.LIST.equals( type ) || ModelDefault.SET.equals( type ) ) { writeTypeField( generator, toType ); } else { // Map or Properties writeTypeField( generator, "object" ); generator.writeObjectFieldStart( "properties" ); XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata) modelAssociation.getAssociationMetadata( XmlAssociationMetadata.ID ); if ( xmlAssociationMetadata.isMapExplode() ) { // key generator.writeObjectFieldStart( "key" ); writeTypeField( generator, "string" ); generator.writeEndObject(); // value generator.writeObjectFieldStart( "value" ); writeTypeField( generator, toType ); generator.writeEndObject(); // properties generator.writeEndObject(); // required field generator.writeArrayFieldStart( "required" ); generator.writeString( "key" ); generator.writeString( "value" ); generator.writeEndArray(); } else { generator.writeObjectFieldStart( "*" ); writeTypeField( generator, toType ); generator.writeEndObject(); } } // items generator.writeEndObject(); } } else { writeTypeField( generator, modelField.getType() ); } generator.writeEndObject(); } if ( reference.hasSuperClass() ) { reference = reference.getModel().getClass( reference.getSuperClass(), getGeneratedVersion() ); } else { reference = null; } } // end of `properties` element generator.writeEndObject(); // write `required` sequence if ( !required.isEmpty() ) { generator.writeArrayFieldStart( "required" ); for ( String requiredField : required ) { generator.writeString( requiredField ); } generator.writeEndArray(); } // end definition if ( !isRoot ) { generator.writeEndObject(); } } private static void writeDescriptionField( JsonGenerator generator, String description ) throws IOException { if ( !StringUtils.isEmpty( description ) ) { generator.writeStringField( "description", description ); } } private void writeTypeField( JsonGenerator generator, String type ) throws IOException { if ( isClassInModel( type, getModel() ) ) { generator.writeStringField( "$ref", "#/definitions/" + type ); return; } // try to make the input type compliant, as much as possible, to JSON Schema primitive types // see http://json-schema.org/latest/json-schema-core.html#anchor8 if ( "boolean".equals( type ) || "Boolean".equals( type ) ) { type = "boolean"; } else if ( "int".equals( type ) || "Integer".equals( type ) ) { type = "integer"; } else if ( "short".equals( type ) || "Short".equals( type ) || "long".equals( type ) || "Long".equals( type ) || "double".equals( type ) || "Double".equals( type ) || "float".equals( type ) || "Float".equals( type ) ) { type = "number"; } else if ( "String".equals( type ) ) { type = "string"; } // keep as it is otherwise generator.writeStringField( "type", type ); } }