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 javax.inject.Named;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.nio.charset.StandardCharsets;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Map;
33  
34  import com.fasterxml.jackson.core.JsonFactory;
35  import com.fasterxml.jackson.core.JsonGenerator;
36  import com.fasterxml.jackson.core.JsonGenerator.Feature;
37  import com.fasterxml.jackson.core.json.JsonWriteFeature;
38  import org.codehaus.modello.ModelloException;
39  import org.codehaus.modello.ModelloParameterConstants;
40  import org.codehaus.modello.model.Model;
41  import org.codehaus.modello.model.ModelAssociation;
42  import org.codehaus.modello.model.ModelClass;
43  import org.codehaus.modello.model.ModelDefault;
44  import org.codehaus.modello.model.ModelField;
45  import org.codehaus.modello.plugins.xml.AbstractXmlJavaGenerator;
46  import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
47  import org.codehaus.plexus.util.StringUtils;
48  
49  /**
50   * @author <a href="mailto:simonetripodi@apache.org">Simone Tripodi</a>
51   * @since 1.8
52   */
53  @Named("jsonschema")
54  public final class JsonSchemaGenerator extends AbstractXmlJavaGenerator {
55  
56      @Override
57      public void generate(Model model, Map<String, Object> parameters) throws ModelloException {
58          initialize(model, parameters);
59  
60          try {
61              generateJsonSchema(parameters);
62          } catch (IOException ioe) {
63              throw new ModelloException("Exception while generating JSON Schema.", ioe);
64          }
65      }
66  
67      private void generateJsonSchema(Map<String, Object> parameters) throws IOException, ModelloException {
68          Model objectModel = getModel();
69  
70          File directory = getOutputDirectory();
71  
72          if (isPackageWithVersion()) {
73              directory = new File(directory, getGeneratedVersion().toString());
74          }
75  
76          if (!directory.exists()) {
77              directory.mkdirs();
78          }
79  
80          // we assume parameters not null
81          String schemaFileName = (String) parameters.get(ModelloParameterConstants.OUTPUT_JSONSCHEMA_FILE_NAME);
82  
83          File schemaFile;
84  
85          if (schemaFileName != null) {
86              schemaFile = new File(directory, schemaFileName);
87          } else {
88              schemaFile = new File(directory, objectModel.getId() + "-" + getGeneratedVersion() + ".schema.json");
89          }
90  
91          JsonGenerator generator = new JsonFactory()
92                  .enable(Feature.AUTO_CLOSE_JSON_CONTENT)
93                  .enable(Feature.AUTO_CLOSE_TARGET)
94                  .enable(Feature.FLUSH_PASSED_TO_STREAM)
95                  .enable(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature())
96                  .enable(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature())
97                  .enable(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature())
98                  .disable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS.mappedFeature())
99                  .createGenerator(newWriter(schemaFile.toPath(), StandardCharsets.UTF_8));
100 
101         generator.useDefaultPrettyPrinter();
102 
103         ModelClass root = objectModel.getClass(objectModel.getRoot(getGeneratedVersion()), getGeneratedVersion());
104 
105         try {
106             generator.writeStartObject();
107             generator.writeStringField("$schema", "http://json-schema.org/draft-04/schema#");
108 
109             writeClassDocumentation(generator, root, true);
110 
111             generator.writeObjectFieldStart("definitions");
112 
113             for (ModelClass current : objectModel.getClasses(getGeneratedVersion())) {
114                 if (!root.equals(current)) {
115                     writeClassDocumentation(generator, current, false);
116                 }
117             }
118 
119             // end "definitions"
120             generator.writeEndObject();
121 
122             // end main object
123             generator.writeEndObject();
124         } finally {
125             generator.close();
126         }
127     }
128 
129     private void writeClassDocumentation(JsonGenerator generator, ModelClass modelClass, boolean isRoot)
130             throws IOException {
131         if (!isRoot) {
132             generator.writeObjectFieldStart(modelClass.getName());
133         }
134 
135         generator.writeStringField("id", modelClass.getName() + '#');
136         writeDescriptionField(generator, modelClass.getDescription());
137         writeTypeField(generator, "object");
138 
139         generator.writeObjectFieldStart("properties");
140 
141         List<String> required = new LinkedList<>();
142 
143         ModelClass reference = modelClass;
144         // traverse the whole modelClass hierarchy to create the nested Builder instance
145         while (reference != null) {
146             // collect parameters and set them in the instance object
147             for (ModelField modelField : reference.getFields(getGeneratedVersion())) {
148                 if (modelField.isRequired()) {
149                     required.add(modelField.getName());
150                 }
151 
152                 // each field is represented as object
153                 generator.writeObjectFieldStart(modelField.getName());
154 
155                 writeDescriptionField(generator, modelField.getDescription());
156 
157                 if (modelField instanceof ModelAssociation) {
158                     ModelAssociation modelAssociation = (ModelAssociation) modelField;
159 
160                     if (modelAssociation.isOneMultiplicity()) {
161                         writeTypeField(generator, modelAssociation.getType());
162                     } else {
163                         // MANY_MULTIPLICITY
164                         writeTypeField(generator, "array");
165 
166                         generator.writeObjectFieldStart("items");
167 
168                         String type = modelAssociation.getType();
169                         String toType = modelAssociation.getTo();
170 
171                         if (ModelDefault.LIST.equals(type) || ModelDefault.SET.equals(type)) {
172                             writeTypeField(generator, toType);
173                         } else {
174                             // Map or Properties
175 
176                             writeTypeField(generator, "object");
177 
178                             generator.writeObjectFieldStart("properties");
179 
180                             XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata)
181                                     modelAssociation.getAssociationMetadata(XmlAssociationMetadata.ID);
182 
183                             if (xmlAssociationMetadata.isMapExplode()) {
184                                 // key
185                                 generator.writeObjectFieldStart("key");
186                                 writeTypeField(generator, "string");
187                                 generator.writeEndObject();
188 
189                                 // value
190                                 generator.writeObjectFieldStart("value");
191                                 writeTypeField(generator, toType);
192                                 generator.writeEndObject();
193 
194                                 // properties
195                                 generator.writeEndObject();
196 
197                                 // required field
198                                 generator.writeArrayFieldStart("required");
199                                 generator.writeString("key");
200                                 generator.writeString("value");
201                                 generator.writeEndArray();
202                             } else {
203                                 generator.writeObjectFieldStart("*");
204                                 writeTypeField(generator, toType);
205                                 generator.writeEndObject();
206                             }
207                         }
208 
209                         // items
210                         generator.writeEndObject();
211                     }
212                 } else {
213                     writeTypeField(generator, modelField.getType());
214                 }
215 
216                 generator.writeEndObject();
217             }
218 
219             if (reference.hasSuperClass()) {
220                 reference = reference.getModel().getClass(reference.getSuperClass(), getGeneratedVersion());
221             } else {
222                 reference = null;
223             }
224         }
225 
226         // end of `properties` element
227         generator.writeEndObject();
228 
229         // write `required` sequence
230         if (!required.isEmpty()) {
231             generator.writeArrayFieldStart("required");
232 
233             for (String requiredField : required) {
234                 generator.writeString(requiredField);
235             }
236 
237             generator.writeEndArray();
238         }
239 
240         // end definition
241         if (!isRoot) {
242             generator.writeEndObject();
243         }
244     }
245 
246     private static void writeDescriptionField(JsonGenerator generator, String description) throws IOException {
247         if (!StringUtils.isEmpty(description)) {
248             generator.writeStringField("description", description);
249         }
250     }
251 
252     private void writeTypeField(JsonGenerator generator, String type) throws IOException {
253         if (isClassInModel(type, getModel())) {
254             generator.writeStringField("$ref", "#/definitions/" + type);
255             return;
256         }
257 
258         // try to make the input type compliant, as much as possible, to JSON Schema primitive types
259         // see http://json-schema.org/latest/json-schema-core.html#anchor8
260         if ("boolean".equals(type) || "Boolean".equals(type)) {
261             type = "boolean";
262         } else if ("int".equals(type) || "Integer".equals(type)) {
263             type = "integer";
264         } else if ("short".equals(type)
265                 || "Short".equals(type)
266                 || "long".equals(type)
267                 || "Long".equals(type)
268                 || "double".equals(type)
269                 || "Double".equals(type)
270                 || "float".equals(type)
271                 || "Float".equals(type)) {
272             type = "number";
273         } else if ("String".equals(type)) {
274             type = "string";
275         }
276 
277         // keep as it is otherwise
278 
279         generator.writeStringField("type", type);
280     }
281 }