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