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