View Javadoc

1   package org.codehaus.modello.plugin.jsonschema;
2   
3   /*
4    * Copyright (c) 2013, Codehaus.org
5    *
6    * Permission is hereby granted, free of charge, to any person obtaining a copy of
7    * this software and associated documentation files (the "Software"), to deal in
8    * the Software without restriction, including without limitation the rights to
9    * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10   * of the Software, and to permit persons to whom the Software is furnished to do
11   * so, subject to the following conditions:
12   *
13   * The above copyright notice and this permission notice shall be included in all
14   * copies or substantial portions of the Software.
15   *
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22   * SOFTWARE.
23   */
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Properties;
30  
31  import org.codehaus.modello.ModelloException;
32  import org.codehaus.modello.ModelloParameterConstants;
33  import org.codehaus.modello.model.Model;
34  import org.codehaus.modello.model.ModelAssociation;
35  import org.codehaus.modello.model.ModelClass;
36  import org.codehaus.modello.model.ModelDefault;
37  import org.codehaus.modello.model.ModelField;
38  import org.codehaus.modello.plugins.xml.AbstractXmlJavaGenerator;
39  import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
40  import org.codehaus.plexus.util.StringUtils;
41  
42  import com.fasterxml.jackson.core.JsonEncoding;
43  import com.fasterxml.jackson.core.JsonFactory;
44  import com.fasterxml.jackson.core.JsonGenerator;
45  import com.fasterxml.jackson.core.JsonGenerator.Feature;
46  
47  /**
48   * @author <a href="mailto:simonetripodi@apache.org">Simone Tripodi</a>
49   * @since 1.8
50   */
51  public final class JsonSchemaGenerator
52      extends AbstractXmlJavaGenerator
53  {
54  
55      public void generate( Model model, Properties parameters )
56          throws ModelloException
57      {
58          initialize( model, parameters );
59  
60          try
61          {
62              generateJsonSchema( parameters );
63          }
64          catch ( IOException ioe )
65          {
66              throw new ModelloException( "Exception while generating JSON Schema.", ioe );
67          }
68      }
69  
70      private void generateJsonSchema( Properties parameters )
71          throws IOException, ModelloException
72      {
73          Model objectModel = getModel();
74  
75          File directory = getOutputDirectory();
76  
77          if ( isPackageWithVersion() )
78          {
79              directory = new File( directory, getGeneratedVersion().toString() );
80          }
81  
82          if ( !directory.exists() )
83          {
84              directory.mkdirs();
85          }
86  
87          // we assume parameters not null
88          String schemaFileName = parameters.getProperty( ModelloParameterConstants.OUTPUT_JSONSCHEMA_FILE_NAME );
89  
90          File schemaFile;
91  
92          if ( schemaFileName != null )
93          {
94              schemaFile = new File( directory, schemaFileName );
95          }
96          else
97          {
98              schemaFile = new File( directory, objectModel.getId() + "-" + getGeneratedVersion() + ".schema.json" );
99          }
100 
101         JsonGenerator generator = new JsonFactory()
102                                   .enable( Feature.AUTO_CLOSE_JSON_CONTENT )
103                                   .enable( Feature.AUTO_CLOSE_TARGET )
104                                   .enable( Feature.ESCAPE_NON_ASCII )
105                                   .enable( Feature.FLUSH_PASSED_TO_STREAM )
106                                   .enable( Feature.QUOTE_FIELD_NAMES )
107                                   .enable( Feature.QUOTE_NON_NUMERIC_NUMBERS )
108                                   .disable( Feature.WRITE_NUMBERS_AS_STRINGS )
109                                   .createGenerator( schemaFile, JsonEncoding.UTF8 );
110 
111         generator.useDefaultPrettyPrinter();
112 
113         ModelClass root = objectModel.getClass( objectModel.getRoot( getGeneratedVersion() ),
114                                                 getGeneratedVersion() );
115 
116         try
117         {
118             generator.writeStartObject();
119             generator.writeStringField( "$schema", "http://json-schema.org/draft-04/schema#" );
120 
121             writeClassDocumentation( generator, root, true );
122 
123             generator.writeObjectFieldStart( "definitions" );
124 
125             for ( ModelClass current : objectModel.getClasses( getGeneratedVersion() ) )
126             {
127                 if ( !root.equals( current ) )
128                 {
129                     writeClassDocumentation( generator, current, false );
130                 }
131             }
132 
133             // end "definitions"
134             generator.writeEndObject();
135 
136             // end main object
137             generator.writeEndObject();
138         }
139         finally
140         {
141             generator.close();
142         }
143     }
144 
145     private void writeClassDocumentation( JsonGenerator generator, ModelClass modelClass, boolean isRoot )
146         throws IOException
147     {
148         if ( !isRoot )
149         {
150             generator.writeObjectFieldStart( modelClass.getName() );
151         }
152 
153         generator.writeStringField( "id", modelClass.getName() + '#' );
154         writeDescriptionField( generator, modelClass.getDescription() );
155         writeTypeField( generator, "object" );
156 
157         generator.writeObjectFieldStart( "properties" );
158 
159         List<String> required = new LinkedList<String>();
160 
161         ModelClass reference = modelClass;
162         // traverse the whole modelClass hierarchy to create the nested Builder instance
163         while ( reference != null )
164         {
165             // collect parameters and set them in the instance object
166             for ( ModelField modelField : reference.getFields( getGeneratedVersion() ) )
167             {
168                 if ( modelField.isRequired() )
169                 {
170                     required.add( modelField.getName() );
171                 }
172 
173                 // each field is represented as object
174                 generator.writeObjectFieldStart( modelField.getName() );
175 
176                 writeDescriptionField( generator, modelField.getDescription() );
177 
178                 if ( modelField instanceof ModelAssociation )
179                 {
180                     ModelAssociation modelAssociation = (ModelAssociation) modelField;
181 
182                     if ( modelAssociation.isOneMultiplicity() )
183                     {
184                         writeTypeField( generator, modelAssociation.getType() );
185                     }
186                     else
187                     {
188                         // MANY_MULTIPLICITY
189                         writeTypeField( generator, "array" );
190 
191                         generator.writeObjectFieldStart( "items" );
192 
193                         String type = modelAssociation.getType();
194                         String toType = modelAssociation.getTo();
195 
196                         if ( ModelDefault.LIST.equals( type ) || ModelDefault.SET.equals( type ) )
197                         {
198                             writeTypeField( generator, toType );
199                         }
200                         else
201                         {
202                             // Map or Properties
203 
204                             writeTypeField( generator, "object" );
205 
206                             generator.writeObjectFieldStart( "properties" );
207 
208                             XmlAssociationMetadata xmlAssociationMetadata =
209                                 (XmlAssociationMetadata) modelAssociation.getAssociationMetadata( XmlAssociationMetadata.ID );
210 
211                             if ( xmlAssociationMetadata.isMapExplode() )
212                             {
213                                 // key
214                                 generator.writeObjectFieldStart( "key" );
215                                 writeTypeField( generator, "string" );
216                                 generator.writeEndObject();
217 
218                                 // value
219                                 generator.writeObjectFieldStart( "value" );
220                                 writeTypeField( generator, toType );
221                                 generator.writeEndObject();
222 
223                                 // properties
224                                 generator.writeEndObject();
225 
226                                 // required field
227                                 generator.writeArrayFieldStart( "required" );
228                                 generator.writeString( "key" );
229                                 generator.writeString( "value" );
230                                 generator.writeEndArray();
231                             }
232                             else
233                             {
234                                 generator.writeObjectFieldStart( "*" );
235                                 writeTypeField( generator, toType );
236                                 generator.writeEndObject();
237                             }
238                         }
239 
240                         // items
241                         generator.writeEndObject();
242                     }
243                 }
244                 else
245                 {
246                     writeTypeField( generator, modelField.getType() );
247                 }
248 
249                 generator.writeEndObject();
250             }
251 
252             if ( reference.hasSuperClass() )
253             {
254                 reference = reference.getModel().getClass( reference.getSuperClass(), getGeneratedVersion() );
255             }
256             else
257             {
258                 reference = null;
259             }
260         }
261 
262         // end of `properties` element
263         generator.writeEndObject();
264 
265         // write `required` sequence
266         if ( !required.isEmpty() )
267         {
268             generator.writeArrayFieldStart( "required" );
269 
270             for ( String requiredField : required )
271             {
272                 generator.writeString( requiredField );
273             }
274 
275             generator.writeEndArray();
276         }
277 
278         // end definition
279         if ( !isRoot )
280         {
281             generator.writeEndObject();
282         }
283     }
284 
285     private static void writeDescriptionField( JsonGenerator generator, String description )
286         throws IOException
287     {
288         if ( !StringUtils.isEmpty( description ) )
289         {
290             generator.writeStringField( "description", description );
291         }
292     }
293 
294     private void writeTypeField( JsonGenerator generator, String type )
295         throws IOException
296     {
297         if ( isClassInModel( type, getModel() ) )
298         {
299             generator.writeStringField( "$ref", "#/definitions/" + type );
300             return;
301         }
302 
303         // try to make the input type compliant, as much as possible, to JSON Schema primitive types
304         // see http://json-schema.org/latest/json-schema-core.html#anchor8
305         if ( "boolean".equals( type ) || "Boolean".equals( type ) )
306         {
307             type = "boolean";
308         }
309         else if ( "int".equals( type ) || "Integer".equals( type ) )
310         {
311             type = "integer";
312         }
313         else if ( "short".equals( type ) || "Short".equals( type )
314                   || "long".equals( type ) || "Long".equals( type )
315                   || "double".equals( type ) || "Double".equals( type )
316                   || "float".equals( type ) || "Float".equals( type ) )
317         {
318             type = "number";
319         }
320         else if ( "String".equals( type ) )
321         {
322             type = "string";
323         }
324 
325         // keep as it is otherwise
326 
327         generator.writeStringField( "type", type );
328     }
329 
330 }