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