View Javadoc
1   package org.codehaus.modello.plugin.xsd;
2   
3   /*
4    * Copyright (c) 2005, 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.io.Writer;
30  import java.nio.charset.StandardCharsets;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Properties;
35  import java.util.Set;
36  
37  import org.codehaus.modello.ModelloException;
38  import org.codehaus.modello.ModelloParameterConstants;
39  import org.codehaus.modello.model.Model;
40  import org.codehaus.modello.model.ModelAssociation;
41  import org.codehaus.modello.model.ModelClass;
42  import org.codehaus.modello.model.ModelField;
43  import org.codehaus.modello.plugin.xsd.metadata.XsdClassMetadata;
44  import org.codehaus.modello.plugins.xml.AbstractXmlGenerator;
45  import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
46  import org.codehaus.modello.plugins.xml.metadata.XmlFieldMetadata;
47  import org.codehaus.plexus.util.StringUtils;
48  import org.codehaus.plexus.util.io.CachingWriter;
49  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
50  import org.codehaus.plexus.util.xml.XMLWriter;
51  
52  /**
53   * @author <a href="mailto:brett@codehaus.org">Brett Porter</a>
54   */
55  @Named("xsd")
56  public class XsdGenerator extends AbstractXmlGenerator {
57      /**
58       * Value standing for any element name (used on xml.tagName)
59       */
60      private static final String ANY_NAME = "*";
61  
62      protected static final String LS = System.lineSeparator();
63  
64      @Override
65      public void generate(Model model, Map<String, Object> parameters) throws ModelloException {
66          initialize(model, parameters);
67  
68          try {
69              generateXsd(parameters);
70          } catch (IOException ex) {
71              throw new ModelloException("Exception while generating xsd.", ex);
72          }
73      }
74  
75      private void generateXsd(Map<String, Object> parameters) throws IOException, ModelloException {
76          Model objectModel = getModel();
77  
78          File directory = getOutputDirectory();
79  
80          if (isPackageWithVersion()) {
81              directory = new File(directory, getGeneratedVersion().toString());
82          }
83  
84          if (!directory.exists()) {
85              directory.mkdirs();
86          }
87  
88          // we assume parameters not null
89          String xsdFileName = (String) parameters.get(ModelloParameterConstants.OUTPUT_XSD_FILE_NAME);
90          boolean enforceMandatoryElements =
91                  Boolean.parseBoolean((String) parameters.get(ModelloParameterConstants.XSD_ENFORCE_MANDATORY_ELEMENTS));
92  
93          File f = new File(directory, objectModel.getId() + "-" + getGeneratedVersion() + ".xsd");
94  
95          if (xsdFileName != null) {
96              f = new File(directory, xsdFileName);
97          }
98  
99          try (Writer writer = new CachingWriter(f, StandardCharsets.UTF_8)) {
100             XMLWriter w = new PrettyPrintXMLWriter(writer);
101 
102             writer.append("<?xml version=\"1.0\"?>").write(LS);
103 
104             initHeader(w);
105 
106             // TODO: the writer should be knowledgeable of namespaces, but this works
107             w.startElement("xs:schema");
108             w.addAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
109             w.addAttribute("elementFormDefault", "qualified");
110 
111             ModelClass root = objectModel.getClass(objectModel.getRoot(getGeneratedVersion()), getGeneratedVersion());
112 
113             String namespace = XsdModelHelper.getNamespace(root.getModel(), getGeneratedVersion());
114 
115             w.addAttribute("xmlns", namespace);
116 
117             String targetNamespace =
118                     XsdModelHelper.getTargetNamespace(root.getModel(), getGeneratedVersion(), namespace);
119 
120             // add targetNamespace if attribute is not blank (specifically set to avoid a target namespace)
121             if (StringUtils.isNotBlank(targetNamespace)) {
122                 w.addAttribute("targetNamespace", targetNamespace);
123             }
124 
125             w.startElement("xs:element");
126             String tagName = resolveTagName(root);
127             w.addAttribute("name", tagName);
128             w.addAttribute("type", root.getName());
129 
130             writeClassDocumentation(w, root);
131 
132             w.endElement();
133 
134             // Element descriptors
135             // Traverse from root so "abstract" models aren't included
136             int initialCapacity = objectModel.getClasses(getGeneratedVersion()).size();
137             writeComplexTypeDescriptor(w, objectModel, root, new HashSet<>(initialCapacity), enforceMandatoryElements);
138 
139             w.endElement();
140         }
141     }
142 
143     private static void writeClassDocumentation(XMLWriter w, ModelClass modelClass) {
144         writeDocumentation(w, modelClass.getVersionRange().toString(), modelClass.getDescription());
145     }
146 
147     private static void writeFieldDocumentation(XMLWriter w, ModelField field) {
148         writeDocumentation(w, field.getVersionRange().toString(), field.getDescription());
149     }
150 
151     private static void writeDocumentation(XMLWriter w, String version, String description) {
152         if (version != null || description != null) {
153             w.startElement("xs:annotation");
154 
155             if (version != null) {
156                 w.startElement("xs:documentation");
157                 w.addAttribute("source", "version");
158                 w.writeText(version);
159                 w.endElement();
160             }
161 
162             if (description != null) {
163                 w.startElement("xs:documentation");
164                 w.addAttribute("source", "description");
165                 w.writeText(description);
166                 w.endElement();
167             }
168 
169             w.endElement();
170         }
171     }
172 
173     private void writeComplexTypeDescriptor(
174             XMLWriter w,
175             Model objectModel,
176             ModelClass modelClass,
177             Set<ModelClass> written,
178             boolean enforceMandatoryElements) {
179         written.add(modelClass);
180 
181         w.startElement("xs:complexType");
182         w.addAttribute("name", modelClass.getName());
183 
184         List<ModelField> fields = getFieldsForXml(modelClass, getGeneratedVersion());
185 
186         ModelField contentField = getContentField(fields);
187 
188         boolean hasContentField = contentField != null;
189 
190         List<ModelField> attributeFields = getXmlAttributeFields(fields);
191 
192         fields.removeAll(attributeFields);
193 
194         if (hasContentField) {
195             // yes it's only an extension of xs:string
196             w.startElement("xs:simpleContent");
197 
198             w.startElement("xs:extension");
199 
200             w.addAttribute("base", getXsdType(contentField.getType()));
201         }
202 
203         writeClassDocumentation(w, modelClass);
204 
205         Set<ModelClass> toWrite = new HashSet<>();
206 
207         if (!fields.isEmpty()) {
208             XsdClassMetadata xsdClassMetadata = (XsdClassMetadata) modelClass.getMetadata(XsdClassMetadata.ID);
209             boolean compositorAll = XsdClassMetadata.COMPOSITOR_ALL.equals(xsdClassMetadata.getCompositor());
210 
211             if (!hasContentField) {
212                 if (compositorAll) {
213                     w.startElement("xs:all");
214                 } else {
215                     w.startElement("xs:sequence");
216                 }
217             }
218 
219             for (ModelField field : fields) {
220                 XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) field.getMetadata(XmlFieldMetadata.ID);
221 
222                 String fieldTagName = resolveTagName(field, xmlFieldMetadata);
223 
224                 if (!hasContentField) {
225                     if (fieldTagName.equals(ANY_NAME)) {
226                         w.startElement("xs:any");
227                         w.addAttribute("minOccurs", "0");
228                         w.addAttribute("maxOccurs", "unbounded");
229                         w.addAttribute("processContents", "skip");
230                         w.endElement();
231                         continue;
232                     }
233                     w.startElement("xs:element");
234 
235                     if (!enforceMandatoryElements || !field.isRequired()) {
236                         // Usually, would only do this if the field is not "required", but due to inheritance, it may be
237                         // present, even if not here, so we need to let it slide
238                         w.addAttribute("minOccurs", "0");
239                     }
240                 }
241 
242                 String xsdType = getXsdType(field.getType());
243 
244                 if ("Date".equals(field.getType()) && "long".equals(xmlFieldMetadata.getFormat())) {
245                     xsdType = getXsdType("long");
246                 }
247 
248                 if (xmlFieldMetadata.isContent()) {
249                     // nothing to add
250                 } else if ((xsdType != null) || "char".equals(field.getType()) || "Character".equals(field.getType())) {
251                     w.addAttribute("name", fieldTagName);
252 
253                     if (xsdType != null) {
254                         // schema built-in datatype
255                         w.addAttribute("type", xsdType);
256                     }
257 
258                     if (field.getDefaultValue() != null) {
259                         // \0 is the implicit default value for char/Character but \0 isn't a valid XML char
260                         if (field.getDefaultValue() != "\0") {
261                             w.addAttribute("default", field.getDefaultValue());
262                         }
263                     }
264 
265                     writeFieldDocumentation(w, field);
266 
267                     if (xsdType == null) {
268                         writeCharElement(w);
269                     }
270                 } else {
271                     // TODO cleanup/split this part it's no really human readable :-)
272                     if (isInnerAssociation(field)) {
273                         ModelAssociation association = (ModelAssociation) field;
274                         ModelClass fieldModelClass = objectModel.getClass(association.getTo(), getGeneratedVersion());
275 
276                         toWrite.add(fieldModelClass);
277 
278                         if (association.isManyMultiplicity()) {
279                             XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata)
280                                     association.getAssociationMetadata(XmlAssociationMetadata.ID);
281 
282                             if (xmlAssociationMetadata.isWrappedItems()) {
283                                 w.addAttribute("name", fieldTagName);
284                                 writeFieldDocumentation(w, field);
285 
286                                 writeListElement(
287                                         w, xmlFieldMetadata, xmlAssociationMetadata, field, fieldModelClass.getName());
288                             } else {
289                                 if (compositorAll) {
290                                     // xs:all does not accept maxOccurs="unbounded", xs:sequence MUST be used
291                                     // to be able to represent this constraint
292                                     throw new IllegalStateException(
293                                             field.getName() + " field is declared as xml.listStyle=\"flat\" "
294                                                     + "then class " + modelClass.getName()
295                                                     + " MUST be declared as xsd.compositor=\"sequence\"");
296                                 }
297 
298                                 w.addAttribute("name", resolveTagName(fieldTagName, xmlAssociationMetadata));
299 
300                                 w.addAttribute("type", fieldModelClass.getName());
301                                 w.addAttribute("maxOccurs", "unbounded");
302 
303                                 writeFieldDocumentation(w, field);
304                             }
305                         } else {
306                             // not many multiplicity
307                             w.addAttribute("name", fieldTagName);
308                             w.addAttribute("type", fieldModelClass.getName());
309                             writeFieldDocumentation(w, field);
310                         }
311                     } else // not inner association
312                     {
313                         w.addAttribute("name", fieldTagName);
314 
315                         writeFieldDocumentation(w, field);
316 
317                         if (List.class.getName().equals(field.getType())
318                                 || Set.class.getName().equals(field.getType())) {
319                             ModelAssociation association = (ModelAssociation) field;
320 
321                             XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata)
322                                     association.getAssociationMetadata(XmlAssociationMetadata.ID);
323 
324                             writeListElement(w, xmlFieldMetadata, xmlAssociationMetadata, field, getXsdType("String"));
325                         } else if (Properties.class.getName().equals(field.getType())
326                                 || "DOM".equals(field.getType())) {
327                             writePropertiesElement(w);
328                         } else {
329                             throw new IllegalStateException("Non-association field of a non-primitive type '"
330                                     + field.getType() + "' for '" + field.getName() + "' in '"
331                                     + modelClass.getName() + "' model class");
332                         }
333                     }
334                 }
335                 if (!hasContentField) {
336                     w.endElement();
337                 }
338             } // end fields iterator
339 
340             if (!hasContentField) {
341                 w.endElement(); // xs:all or xs:sequence
342             }
343         }
344 
345         for (ModelField field : attributeFields) {
346             XmlFieldMetadata xmlFieldMetadata = (XmlFieldMetadata) field.getMetadata(XmlFieldMetadata.ID);
347 
348             w.startElement("xs:attribute");
349 
350             String xsdType = getXsdType(field.getType());
351 
352             String tagName = resolveTagName(field, xmlFieldMetadata);
353 
354             w.addAttribute("name", tagName);
355 
356             if (xsdType != null) {
357                 w.addAttribute("type", xsdType);
358             }
359 
360             if (field.getDefaultValue() != null) {
361                 w.addAttribute("default", field.getDefaultValue());
362             }
363 
364             w.addAttribute("use", field.isRequired() ? "required" : "optional");
365 
366             writeFieldDocumentation(w, field);
367 
368             if ("char".equals(field.getType()) || "Character".equals(field.getType())) {
369                 writeCharElement(w);
370             } else if (xsdType == null) {
371                 throw new IllegalStateException("Attribute field of a non-primitive type '" + field.getType()
372                         + "' for '" + field.getName() + "' in '" + modelClass.getName() + "' model class");
373             }
374 
375             w.endElement();
376         }
377 
378         if (hasContentField) {
379             w.endElement(); // xs:extension
380 
381             w.endElement(); // xs:simpleContent
382         }
383 
384         w.endElement(); // xs:complexType
385 
386         for (ModelClass fieldModelClass : toWrite) {
387             if (!written.contains(fieldModelClass)) {
388                 writeComplexTypeDescriptor(w, objectModel, fieldModelClass, written, enforceMandatoryElements);
389             }
390         }
391     }
392 
393     private static void writeCharElement(XMLWriter w) {
394         // a char, described as a simpleType base on string with a length restriction to 1
395         w.startElement("xs:simpleType");
396 
397         w.startElement("xs:restriction");
398         w.addAttribute("base", "xs:string");
399 
400         w.startElement("xs:length");
401         w.addAttribute("value", "1");
402         w.addAttribute("fixed", "true");
403 
404         w.endElement();
405 
406         w.endElement();
407 
408         w.endElement();
409     }
410 
411     private static void writePropertiesElement(XMLWriter w) {
412         w.startElement("xs:complexType");
413 
414         w.startElement("xs:sequence");
415 
416         w.startElement("xs:any");
417         w.addAttribute("minOccurs", "0");
418         w.addAttribute("maxOccurs", "unbounded");
419         w.addAttribute("processContents", "skip");
420 
421         w.endElement();
422 
423         w.endElement();
424 
425         w.endElement();
426     }
427 
428     private void writeListElement(
429             XMLWriter w,
430             XmlFieldMetadata xmlFieldMetadata,
431             XmlAssociationMetadata xmlAssociationMetadata,
432             ModelField field,
433             String type) {
434         String fieldTagName = resolveTagName(field, xmlFieldMetadata);
435 
436         String valuesTagName = resolveTagName(fieldTagName, xmlAssociationMetadata);
437 
438         w.startElement("xs:complexType");
439 
440         w.startElement("xs:sequence");
441 
442         if (valuesTagName.equals(ANY_NAME)) {
443             w.startElement("xs:any");
444             w.addAttribute("processContents", "skip");
445         } else {
446             w.startElement("xs:element");
447             w.addAttribute("type", type);
448             w.addAttribute("name", valuesTagName);
449         }
450         w.addAttribute("minOccurs", "0");
451         w.addAttribute("maxOccurs", "unbounded");
452 
453         w.endElement();
454 
455         w.endElement();
456 
457         w.endElement();
458     }
459 
460     private static String getXsdType(String type) {
461         if ("String".equals(type)) {
462             return "xs:string";
463         } else if ("boolean".equals(type) || "Boolean".equals(type)) {
464             return "xs:boolean";
465         } else if ("byte".equals(type) || "Byte".equals(type)) {
466             return "xs:byte";
467         } else if ("short".equals(type) || "Short".equals(type)) {
468             return "xs:short";
469         } else if ("int".equals(type) || "Integer".equals(type)) {
470             return "xs:int";
471         } else if ("long".equals(type) || "Long".equals(type)) {
472             return "xs:long";
473         } else if ("float".equals(type) || "Float".equals(type)) {
474             return "xs:float";
475         } else if ("double".equals(type) || "Double".equals(type)) {
476             return "xs:double";
477         } else if ("Date".equals(type)) {
478             return "xs:dateTime";
479         } else {
480             return null;
481         }
482     }
483 }