View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.rat.tools.xsd;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStreamWriter;
26  import java.io.Writer;
27  import java.nio.charset.StandardCharsets;
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  
33  import javax.xml.transform.OutputKeys;
34  import javax.xml.transform.Transformer;
35  import javax.xml.transform.TransformerException;
36  import javax.xml.transform.TransformerFactory;
37  import javax.xml.transform.stream.StreamResult;
38  import javax.xml.transform.stream.StreamSource;
39  
40  import org.apache.rat.commandline.StyleSheets;
41  import org.apache.rat.config.parameters.ComponentType;
42  import org.apache.rat.config.parameters.Description;
43  import org.apache.rat.config.parameters.DescriptionBuilder;
44  import org.apache.rat.configuration.MatcherBuilderTracker;
45  import org.apache.rat.configuration.XMLConfig;
46  import org.apache.rat.license.SimpleLicense;
47  import org.apache.rat.tools.xsd.XsdWriter.Type;
48  
49  /**
50   * Generates the XSD for a configuration.
51   */
52  public class XsdGenerator {
53      /** The XsdWriter being written to. */
54      private XsdWriter writer;
55      /** A map of component type to XML element name / property type */
56      private static final Map<ComponentType, String> TYPE_MAP = new HashMap<>();
57  
58      static {
59          TYPE_MAP.put(ComponentType.MATCHER, XMLConfig.MATCHER);
60          TYPE_MAP.put(ComponentType.PARAMETER, "xs:string");
61          TYPE_MAP.put(ComponentType.LICENSE, XMLConfig.LICENSE);
62      }
63  
64      /**
65       * Command line that accepts standard RAT CLI command line options and generates an XSD from the
66       * configuration.
67       * @param args the arguments for RAT CLI.
68       * @throws IOException on IO errors.
69       * @throws TransformerException if the XSD can not be pretty printed.
70       */
71      public static void main(final String[] args) throws IOException, TransformerException {
72          XsdGenerator generator = new XsdGenerator();
73  
74          TransformerFactory tf = TransformerFactory.newInstance();
75          Transformer transformer;
76          try (InputStream in = generator.getInputStream();
77               InputStream styleIn = StyleSheets.XML.getStyleSheet().get()) {
78              transformer = tf.newTransformer(new StreamSource(styleIn));
79              transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
80              transformer.setOutputProperty(OutputKeys.METHOD, "xml");
81              transformer.setOutputProperty(OutputKeys.INDENT, "yes");
82              transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
83              transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
84              transformer.transform(new StreamSource(in),
85                      new StreamResult(new OutputStreamWriter(System.out, StandardCharsets.UTF_8)));
86          }
87      }
88  
89      /**
90       * Create an input stream from the output of the generator.
91       * @return an InputStream that contains the output of the generator.
92       * @throws IOException on output errors.
93       */
94      public InputStream getInputStream() throws IOException {
95          try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
96               Writer writer = new OutputStreamWriter(baos)) {
97              write(writer);
98              return new ByteArrayInputStream(baos.toByteArray());
99          }
100     }
101 
102     /**
103      * Writes the XSD to the output.
104      * @param output the output to write to.
105      * @throws IOException on write error.
106      */
107     public void write(final Writer output) throws IOException {
108         writer = new XsdWriter(output).init();
109 
110         writer.open(Type.ELEMENT, "name", XMLConfig.ROOT)
111                 .open(Type.COMPLEX).open(Type.SEQUENCE);
112         writeFamilies();
113         writeLicenses();
114         writeApproved();
115         writeMatchers();
116         writer.close(Type.ELEMENT);
117 
118         writeMatcherElements();
119 
120         writer.finish();
121     }
122 
123     private void writeFamilies() throws IOException {
124         writer.open(Type.ELEMENT, "name", XMLConfig.FAMILIES, "maxOccurs", "1", "minOccurs", "0")
125                 .open(Type.COMPLEX)
126                 .open(Type.SEQUENCE)
127                 .open(Type.ELEMENT, "name", XMLConfig.FAMILY, "maxOccurs", "unbounded", "minOccurs", "0")
128                 .open(Type.COMPLEX)
129                 .attribute(XMLConfig.ATT_ID, "type", "xs:string", "use", "required")
130                 .attribute(XMLConfig.ATT_NAME, "type", "xs:string", "use", "required")
131                 .close(Type.ELEMENT)
132                 .close(Type.ELEMENT); // families
133     }
134 
135     private void writeLicenses() throws IOException {
136         Description desc = DescriptionBuilder.buildMap(SimpleLicense.class);
137         List<Description> children = new ArrayList<>();
138         List<Description> attributes = new ArrayList<>();
139 
140         if (desc != null && desc.getChildren() != null) {
141             for (Description child : desc.getChildren().values()) {
142                 if (XMLConfig.isLicenseChild(child.getCommonName())) {
143                     children.add(child);
144                 } else {
145                     if (child.getType() == ComponentType.PARAMETER) {
146                         attributes.add(child);
147                     }
148                 }
149             }
150         }
151         writer.open(Type.ELEMENT, "name", XMLConfig.LICENSES, "maxOccurs", "1", "minOccurs", "0")
152                 .open(Type.COMPLEX).open(Type.SEQUENCE)
153         .open(Type.ELEMENT, "name", XMLConfig.LICENSE, "maxOccurs", "unbounded", "minOccurs", "0")
154                 .open(Type.COMPLEX).open(Type.CHOICE, "maxOccurs", "unbounded", "minOccurs", "1");
155         for (Description child : children) {
156             if (child.getCommonName().equals("matcher")) {
157                 writer.open(Type.ELEMENT, "ref", XMLConfig.MATCHER, "maxOccurs", "1", "minOccurs", "1").close(Type.ELEMENT);
158             } else {
159                 element(child);
160             }
161         }
162         writer.close(Type.CHOICE);
163         for (Description child : attributes) {
164             attribute(child);
165         }
166         writer.close(Type.ELEMENT).close(Type.ELEMENT);
167     }
168 
169     private void writeApproved() throws IOException {
170         writer.open(Type.ELEMENT, "name", XMLConfig.APPROVED, "maxOccurs", "1", "minOccurs", "0")
171                 .open(Type.COMPLEX).open(Type.SEQUENCE)
172                 .open(Type.ELEMENT, "name", XMLConfig.FAMILY, "maxOccurs", "unbounded", "minOccurs", "0")
173                 .open(Type.COMPLEX)
174                 .attribute(XMLConfig.ATT_LICENSE_REF, "type", "xs:string", "use", "required")
175                 .close(Type.ELEMENT)
176                 .close(Type.ELEMENT);
177     }
178 
179     private void writeMatchers() throws IOException {
180         writer.open(Type.ELEMENT, "name", XMLConfig.MATCHERS, "maxOccurs", "1", "minOccurs", "0")
181                 .open(Type.COMPLEX).open(Type.SEQUENCE)
182                 .open(Type.ELEMENT, "name", XMLConfig.MATCHER, "maxOccurs", "unbounded", "minOccurs", "0")
183                 .open(Type.COMPLEX)
184                 .attribute(XMLConfig.ATT_CLASS_NAME, "type", "xs:string", "use", "required")
185                 .close(Type.ELEMENT)
186                 .close(Type.ELEMENT);
187     }
188 
189     private void writeMatcherElements() throws IOException {
190         MatcherBuilderTracker tracker = MatcherBuilderTracker.instance();
191         writer.open(Type.ELEMENT, "name", XMLConfig.MATCHER, "abstract", "true").close(Type.ELEMENT);
192 
193         // matchers
194         for (Class<?> clazz : tracker.getClasses()) {
195             Description desc = DescriptionBuilder.buildMap(clazz);
196             if (desc != null) {
197                 boolean hasResourceAttr = false;
198                 Description inline = null;
199                 List<Description> attributes = new ArrayList<>();
200                 for (Description child : desc.getChildren().values()) {
201                     if (XMLConfig.isInlineNode(desc.getCommonName(), child.getCommonName())) {
202                         inline = child;
203                     } else {
204                         hasResourceAttr |= child.getCommonName().equals(XMLConfig.ATT_RESOURCE);
205                         attributes.add(child);
206                     }
207                 }
208                 writer.open(Type.ELEMENT, "name", desc.getCommonName(), "substitutionGroup", XMLConfig.MATCHER)
209                         .open(Type.COMPLEX);
210                 if (inline != null) {
211                     if ("enclosed".equals(inline.getCommonName())) {
212                         writer.open(Type.CHOICE).open(Type.ELEMENT, "ref", XMLConfig.MATCHER, "maxOccurs",
213                                         inline.isCollection() ? "unbounded" : "1", "minOccurs", hasResourceAttr ? "0" : "1")
214                                 .close(Type.CHOICE);
215                     } else {
216                         writer.open(Type.SIMPLE).open(Type.EXTENSION, "base", "xs:string");
217                     }
218                 }
219                 for (Description child : attributes) {
220                     attribute(child);
221                 }
222                 writer.close(Type.ELEMENT);
223             }
224         }
225     }
226 
227     private void element(final Description desc) throws IOException {
228         String typeName = TYPE_MAP.get(desc.getType());
229         if (typeName != null) {
230             writer.open(Type.ELEMENT,
231                     "name", desc.getCommonName(),
232                     "type", typeName,
233                     "minOccurs", desc.isRequired() ? "1" : "0",
234                     "maxOccurs", desc.isCollection() ? "unbounded" : "1"
235                     ).close(Type.ELEMENT);
236         }
237     }
238 
239     private void attribute(final Description attr) throws IOException {
240         if (attr.getType() == ComponentType.PARAMETER) {
241             writer.attribute(attr.getCommonName(), "form", "unqualified", "use", attr.isRequired() ? "required" : "optional");
242         }
243     }
244 }