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.configuration;
20  
21  import java.io.IOException;
22  import java.lang.reflect.InvocationTargetException;
23  import java.net.URL;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.Map;
27  import java.util.SortedSet;
28  import java.util.TreeSet;
29  import java.util.function.Consumer;
30  
31  import javax.xml.parsers.DocumentBuilder;
32  import javax.xml.parsers.DocumentBuilderFactory;
33  import javax.xml.parsers.ParserConfigurationException;
34  
35  import org.apache.commons.beanutils.MethodUtils;
36  import org.apache.commons.lang3.StringUtils;
37  import org.apache.rat.ConfigurationException;
38  import org.apache.rat.analysis.IHeaderMatcher;
39  import org.apache.rat.analysis.matchers.FullTextMatcher;
40  import org.apache.rat.analysis.matchers.SimpleTextMatcher;
41  import org.apache.rat.configuration.builders.AbstractBuilder;
42  import org.apache.rat.configuration.builders.ChildContainerBuilder;
43  import org.apache.rat.configuration.builders.MatcherRefBuilder;
44  import org.apache.rat.configuration.builders.TextCaptureBuilder;
45  import org.apache.rat.license.ILicense;
46  import org.apache.rat.license.ILicenseFamily;
47  import org.apache.rat.license.LicenseFamilySetFactory;
48  import org.apache.rat.license.LicenseSetFactory;
49  import org.w3c.dom.DOMException;
50  import org.w3c.dom.Document;
51  import org.w3c.dom.Element;
52  import org.w3c.dom.NamedNodeMap;
53  import org.w3c.dom.Node;
54  import org.w3c.dom.NodeList;
55  import org.xml.sax.SAXException;
56  
57  /**
58   * A class that reads the XML configuration file format.
59   * <p>
60   * {@code <rat-config>}<br>
61   * {@code   <licenses>}<br>
62   * {@code     <license id=id name=name >}<br>
63   * {@code       <notes></notes>}<br>
64   * {@code       <text>  </text>}<br>
65   * {@code       <copyright start='' end='' owner=''/>}<br>
66   * {@code       <spdx></spdx> }<br>
67   * {@code       <and> <matcher/>...</and>}<br>
68   * {@code       <or> <matcher/>...</or> }<br>
69   * {@code       <matcher_ref refid='' />}<br>
70   * {@code       <not><matcher /></not>}<br>
71   * {@code     </license>}<br>
72   * {@code   </licenses>}<br>
73   * {@code   <approved>}<br>
74   * {@code     <family refid=''>}<br>
75   * {@code   </approved>}<br>
76   * {@code   <matchers>}<br>
77   * {@code     <matcher className=''/>}<br>
78   * {@code     <matcher className=''/>}<br>
79   * {@code   </matchers>}<br>
80   * {@code </rat-config>}<br>
81   * </p>
82   */
83  
84  public class XMLConfigurationReader implements LicenseReader, MatcherReader {
85  
86      private final static String ATT_ID = "id";
87      private final static String ATT_NAME = "name";
88      private final static String ATT_DERIVED_FROM = "derived_from";
89      private final static String ATT_LICENSE_REF = "license_ref";
90      private final static String ATT_CLASS_NAME = "class";
91  
92      private final static String ROOT = "rat-config";
93      private final static String FAMILIES = "families";
94      private final static String LICENSES = "licenses";
95      private final static String LICENSE = "license";
96      private final static String APPROVED = "approved";
97      private final static String FAMILY = "family";
98      private final static String NOTE = "note";
99      private final static String MATCHERS = "matchers";
100     private final static String MATCHER = "matcher";
101 
102     private Document document;
103     private final Element rootElement;
104     private final Element familiesElement;
105     private final Element licensesElement;
106     private final Element approvedElement;
107     private final Element matchersElement;
108 
109     private final SortedSet<ILicense> licenses;
110     private final Map<String, IHeaderMatcher> matchers;
111     private final SortedSet<ILicenseFamily> licenseFamilies;
112     private final SortedSet<String> approvedFamilies;
113 
114     /**
115      * Constructs the XML configuration reader.
116      */
117     public XMLConfigurationReader() {
118         try {
119             document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
120         } catch (ParserConfigurationException e) {
121             throw new IllegalStateException("No XML parser defined", e);
122         }
123         rootElement = document.createElement(ROOT);
124         document.appendChild(rootElement);
125         familiesElement = document.createElement(FAMILIES);
126         rootElement.appendChild(familiesElement);
127         licensesElement = document.createElement(LICENSES);
128         rootElement.appendChild(licensesElement);
129         approvedElement = document.createElement(APPROVED);
130         rootElement.appendChild(approvedElement);
131         matchersElement = document.createElement(MATCHERS);
132         rootElement.appendChild(matchersElement);
133         licenses = LicenseSetFactory.emptyLicenseSet();
134         licenseFamilies = LicenseFamilySetFactory.emptyLicenseFamilySet();
135         approvedFamilies = new TreeSet<>();
136         matchers = new HashMap<>();
137     }
138 
139     @Override
140     public void addLicenses(URL url) {
141         read(url);
142     }
143 
144     /**
145      * Read the urls and create a single document to process.
146      * 
147      * @param urls The URLs to read.
148      */
149     public void read(URL... urls) {
150         DocumentBuilder builder;
151         try {
152             builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
153         } catch (ParserConfigurationException e) {
154             throw new ConfigurationException("Unable to create DOM builder", e);
155         }
156         for (URL url : urls) {
157             try {
158                 add(builder.parse(url.openStream()));
159             } catch (SAXException | IOException e) {
160                 throw new ConfigurationException("Unable to read url: " + url, e);
161             }
162         }
163     }
164 
165     /**
166      * Applies the {@code consumer} to each node in the {@code list}
167      * 
168      * @param list the NodeList to process
169      * @param consumer the consumer to apply to each node in the list.
170      */
171     private void nodeListConsumer(NodeList list, Consumer<Node> consumer) {
172         for (int i = 0; i < list.getLength(); i++) {
173             consumer.accept(list.item(i));
174         }
175     }
176 
177     /**
178      * Merge the new document into the document that this reader processes.
179      * 
180      * @param newDoc the Document to merge.
181      */
182     public void add(Document newDoc) {
183         nodeListConsumer(newDoc.getElementsByTagName(FAMILIES),
184                 nl -> nodeListConsumer( nl.getChildNodes(), 
185                 n -> familiesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
186         nodeListConsumer(newDoc.getElementsByTagName(LICENSE),
187                 n -> licensesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
188         nodeListConsumer(newDoc.getElementsByTagName(APPROVED),
189                 nl -> nodeListConsumer( nl.getChildNodes(),
190                 n -> approvedElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
191         nodeListConsumer(newDoc.getElementsByTagName(MATCHERS),
192                 n -> matchersElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
193     }
194 
195     /**
196      * Get a map of Node attribute names to values.
197      * 
198      * @param node The node to process
199      * @return the map of attributes on the node
200      */
201     private Map<String, String> attributes(Node node) {
202         NamedNodeMap nnm = node.getAttributes();
203         Map<String, String> result = new HashMap<>();
204         for (int i = 0; i < nnm.getLength(); i++) {
205             Node n = nnm.item(i);
206             result.put(n.getNodeName(), n.getNodeValue());
207         }
208         return result;
209     }
210 
211     /**
212      * Create a text matcher. Will construct a FullTextMatcher or a
213      * SimpleTextMatcher depending on the complexity of the text.
214      * 
215      * @param id the id for the Matcher.
216      * @param txt the text to match
217      * @return the IHeaderMatcher that matches the text.
218      */
219     public static IHeaderMatcher createTextMatcher(String id, String txt) {
220         boolean complex = txt.contains(" ") | txt.contains("\\t") | txt.contains("\\n") | txt.contains("\\r")
221                 | txt.contains("\\f") | txt.contains("\\v");
222         return complex ? new FullTextMatcher(id, txt) : new SimpleTextMatcher(id, txt);
223     }
224 
225     private AbstractBuilder parseMatcher(Node matcherNode) {
226         AbstractBuilder builder = MatcherBuilderTracker.getMatcherBuilder(matcherNode.getNodeName());
227 
228         NamedNodeMap nnm = matcherNode.getAttributes();
229         for (int i = 0; i < nnm.getLength(); i++) {
230             Node n = nnm.item(i);
231             String methodName = "set" + StringUtils.capitalize(n.getNodeName());
232             try {
233                 MethodUtils.invokeExactMethod(builder, methodName, n.getNodeValue());
234             } catch (NoSuchMethodException e) {
235                 throw new ConfigurationException(
236                         String.format("'%s' does not have a setter '%s' that takes a String argument",
237                                 matcherNode.getNodeName(), methodName));
238             } catch (IllegalAccessException | InvocationTargetException | DOMException e) {
239                 throw new ConfigurationException(e);
240             }
241         }
242         if (builder instanceof ChildContainerBuilder) {
243             ChildContainerBuilder ccb = (ChildContainerBuilder) builder;
244             nodeListConsumer(matcherNode.getChildNodes(), x -> {
245                 if (x.getNodeType() == Node.ELEMENT_NODE) {
246                     ccb.add(parseMatcher(x));
247                 }
248             });
249         }
250         if (builder instanceof TextCaptureBuilder) {
251             ((TextCaptureBuilder) builder).setText(matcherNode.getTextContent().trim());
252         }
253 
254         if (builder instanceof MatcherRefBuilder) {
255             ((MatcherRefBuilder) builder).setMatchers(matchers);
256         }
257 
258         if (builder.hasId()) {
259             builder = new DelegatingBuilder(builder) {
260                 @Override
261                 public IHeaderMatcher build() {
262                     IHeaderMatcher result = delegate.build();
263                     matchers.put(result.getId(), result);
264                     return result;
265                 }
266             };
267         }
268         return builder;
269     }
270 
271     private ILicense parseLicense(Node licenseNode) {
272         Map<String, String> attributes = attributes(licenseNode);
273         ILicense.Builder builder = ILicense.builder();
274 
275         builder.setLicenseFamilyCategory(attributes.get(FAMILY));
276         builder.setName(attributes.get(ATT_NAME));
277         builder.setId(attributes.get(ATT_ID));
278 
279         StringBuilder notesBuilder = new StringBuilder();
280         nodeListConsumer(licenseNode.getChildNodes(), x -> {
281             if (x.getNodeType() == Node.ELEMENT_NODE) {
282                 if (x.getNodeName().equals(NOTE)) {
283                     notesBuilder.append(x.getTextContent()).append("\n");
284                 } else {
285                     builder.setMatcher(parseMatcher(x));
286                 }
287             }
288         });
289         builder.setDerivedFrom(StringUtils.defaultIfBlank(attributes.get(ATT_DERIVED_FROM), null));
290         builder.setNotes(StringUtils.defaultIfBlank(notesBuilder.toString().trim(), null));
291         return builder.build(licenseFamilies);
292     }
293 
294     @Override
295     public SortedSet<ILicense> readLicenses() {
296         readFamilies();
297         readMatcherBuilders();
298         if (licenses.isEmpty()) {
299             nodeListConsumer(document.getElementsByTagName(LICENSE), x -> licenses.add(parseLicense(x)));
300             document = null;
301         }
302         return Collections.unmodifiableSortedSet(licenses);
303     }
304     
305 
306     @Override
307     public SortedSet<ILicenseFamily> readFamilies() {
308         if (licenseFamilies.isEmpty()) {
309             nodeListConsumer(document.getElementsByTagName(FAMILIES),
310                     x -> nodeListConsumer(x.getChildNodes(), this::parseFamily));
311             nodeListConsumer(document.getElementsByTagName(APPROVED),
312                     x -> nodeListConsumer(x.getChildNodes(), this::parseApproved));
313         }
314         return Collections.unmodifiableSortedSet(licenseFamilies);
315     }
316     
317     private ILicenseFamily parseFamily(Map<String, String> attributes) {
318         if (attributes.containsKey(ATT_ID)) {
319             ILicenseFamily.Builder builder = ILicenseFamily.builder();
320             builder.setLicenseFamilyCategory(attributes.get(ATT_ID));
321             builder.setLicenseFamilyName(StringUtils.defaultIfBlank(attributes.get(ATT_NAME), attributes.get(ATT_ID)));
322             return builder.build();
323         }
324         return null;
325     }
326 
327     private void parseFamily(Node familyNode) {
328         if (FAMILY.equals(familyNode.getNodeName())) {
329             ILicenseFamily result = parseFamily(attributes(familyNode));
330             if (result == null) {
331                 throw new ConfigurationException(String.format("families/family tag requires %s attribute", ATT_ID));
332             }
333             licenseFamilies.add(result);
334         }
335     }
336 
337     private void parseApproved(Node approvedNode) {
338         if (FAMILY.equals(approvedNode.getNodeName())) {
339             Map<String, String> attributes = attributes(approvedNode);
340             if (attributes.containsKey(ATT_LICENSE_REF)) {
341                 approvedFamilies.add(attributes.get(ATT_LICENSE_REF));
342             } else if (attributes.containsKey(ATT_ID)) {
343                 ILicenseFamily target = parseFamily(attributes);
344                 licenseFamilies.add(target);
345                 approvedFamilies.add(target.getFamilyCategory());
346             } else {
347                 throw new ConfigurationException(
348                         String.format("family tag requires %s or %s attribute", ATT_LICENSE_REF, ATT_ID));
349             }
350         }
351     }
352 
353     @Override
354     public SortedSet<String> approvedLicenseId() {
355         if (licenses.isEmpty()) {
356             this.readLicenses();
357         }
358         if (approvedFamilies.isEmpty()) {
359             SortedSet<String> result = new TreeSet<>();
360             licenses.stream().map(x -> x.getLicenseFamily().getFamilyCategory()).forEach(result::add);
361             return result;
362         }
363         return Collections.unmodifiableSortedSet(approvedFamilies);
364     }
365 
366     private void parseMatcherBuilder(Node classNode) {
367         Map<String, String> attributes = attributes(classNode);
368         if (attributes.get(ATT_CLASS_NAME) == null) {
369             throw new ConfigurationException("matcher must have a " + ATT_CLASS_NAME + " attribute");
370         }
371         MatcherBuilderTracker.addBuilder(attributes.get(ATT_CLASS_NAME), attributes.get(ATT_NAME));
372     }
373 
374     @Override
375     public void readMatcherBuilders() {
376         nodeListConsumer(document.getElementsByTagName(MATCHER), this::parseMatcherBuilder);
377     }
378 
379     @Override
380     public void addMatchers(URL url) {
381         read(url);
382     }
383 
384     /**
385      * An abstract builder that delegates to another abstract builder.
386      */
387     abstract static class DelegatingBuilder extends AbstractBuilder {
388         protected final AbstractBuilder delegate;
389 
390         DelegatingBuilder(AbstractBuilder delegate) {
391             this.delegate = delegate;
392         }
393     }
394 }