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