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.io.InputStream;
23  import java.io.Reader;
24  import java.lang.reflect.InvocationTargetException;
25  import java.lang.reflect.Method;
26  import java.net.URL;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.SortedSet;
34  import java.util.TreeSet;
35  import java.util.function.BiPredicate;
36  import java.util.function.Consumer;
37  import java.util.stream.Collectors;
38  
39  import javax.xml.parsers.DocumentBuilder;
40  import javax.xml.parsers.DocumentBuilderFactory;
41  import javax.xml.parsers.ParserConfigurationException;
42  
43  import org.apache.commons.lang3.StringUtils;
44  import org.apache.commons.lang3.tuple.ImmutablePair;
45  import org.apache.commons.lang3.tuple.Pair;
46  import org.apache.rat.BuilderParams;
47  import org.apache.rat.ConfigurationException;
48  import org.apache.rat.ImplementationException;
49  import org.apache.rat.analysis.IHeaderMatcher;
50  import org.apache.rat.config.parameters.ComponentType;
51  import org.apache.rat.config.parameters.Description;
52  import org.apache.rat.config.parameters.DescriptionBuilder;
53  import org.apache.rat.configuration.builders.AbstractBuilder;
54  import org.apache.rat.license.ILicense;
55  import org.apache.rat.license.ILicenseFamily;
56  import org.apache.rat.license.LicenseFamilySetFactory;
57  import org.apache.rat.license.LicenseSetFactory;
58  import org.apache.rat.utils.DefaultLog;
59  import org.apache.rat.utils.Log;
60  import org.w3c.dom.DOMException;
61  import org.w3c.dom.Document;
62  import org.w3c.dom.Element;
63  import org.w3c.dom.NamedNodeMap;
64  import org.w3c.dom.Node;
65  import org.w3c.dom.NodeList;
66  import org.xml.sax.InputSource;
67  import org.xml.sax.SAXException;
68  
69  /**
70   * A class that reads the XML configuration file format.
71   */
72  public final class XMLConfigurationReader implements LicenseReader, MatcherReader {
73  
74      private Log log;
75      private Document document;
76      private final Element rootElement;
77      private final Element familiesElement;
78      private final Element licensesElement;
79      private final Element approvedElement;
80      private final Element matchersElement;
81  
82      private final SortedSet<ILicense> licenses;
83  
84      private final Map<String, IHeaderMatcher> matchers;
85      private final BuilderParams builderParams;
86      private final SortedSet<ILicenseFamily> licenseFamilies;
87      private final SortedSet<String> approvedFamilies;
88  
89      /**
90       * Constructs the XML configuration reader.
91       */
92      public XMLConfigurationReader() {
93          try {
94              document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
95          } catch (ParserConfigurationException e) {
96              throw new IllegalStateException("No XML parser defined", e);
97          }
98          rootElement = document.createElement(XMLConfig.ROOT);
99          document.appendChild(rootElement);
100         familiesElement = document.createElement(XMLConfig.FAMILIES);
101         rootElement.appendChild(familiesElement);
102         licensesElement = document.createElement(XMLConfig.LICENSES);
103         rootElement.appendChild(licensesElement);
104         approvedElement = document.createElement(XMLConfig.APPROVED);
105         rootElement.appendChild(approvedElement);
106         matchersElement = document.createElement(XMLConfig.MATCHERS);
107         rootElement.appendChild(matchersElement);
108         licenses = LicenseSetFactory.emptyLicenseSet();
109         licenseFamilies = LicenseFamilySetFactory.emptyLicenseFamilySet();
110         approvedFamilies = new TreeSet<>();
111         matchers = new HashMap<>();
112         builderParams = new BuilderParams() {
113             @Override
114             public Map<String, IHeaderMatcher> matcherMap() {
115                 return matchers;
116             }
117 
118             @Override
119             public SortedSet<ILicenseFamily> licenseFamilies() {
120                 return licenseFamilies;
121             }
122         };
123     }
124 
125     @Override
126     public void setLog(Log log) {
127         this.log = log;
128     }
129 
130     /**
131      * Returns the log the reader.
132      * @return the log if set, if not set {@code DefaultLog.getInstance()} is returned.
133      */
134     public Log getLog() {
135         return log == null ? DefaultLog.getInstance() : log;
136     }
137 
138     @Override
139     public void addLicenses(URL url) {
140         read(url);
141     }
142 
143     /**
144      * Read xml from a reader.
145      *
146      * @param reader the reader to read XML from.
147      */
148     public void read(Reader reader) {
149         DocumentBuilder builder;
150         try {
151             builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
152         } catch (ParserConfigurationException e) {
153             throw new ConfigurationException("Unable to create DOM builder", e);
154         }
155 
156         try {
157             add(builder.parse(new InputSource(reader)));
158         } catch (SAXException | IOException e) {
159             throw new ConfigurationException("Unable to read inputSource", e);
160         }
161 
162     }
163 
164     /**
165      * Read the urls and extract the DOM information to create new objects.
166      *
167      * @param urls The URLs to read.
168      */
169     public void read(URL... urls) {
170         DocumentBuilder builder;
171         try {
172             builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
173         } catch (ParserConfigurationException e) {
174             throw new ConfigurationException("Unable to create DOM builder", e);
175         }
176         for (URL url : urls) {
177             try (InputStream inputStream = url.openStream()){
178                 add(builder.parse(inputStream));
179             } catch (SAXException | IOException e) {
180                 throw new ConfigurationException("Unable to read url: " + url, e);
181             }
182         }
183     }
184 
185     /**
186      * Applies the {@code consumer} to each node in the {@code list}
187      *
188      * @param list the NodeList to process
189      * @param consumer the consumer to apply to each node in the list.
190      */
191     private void nodeListConsumer(NodeList list, Consumer<Node> consumer) {
192         for (int i = 0; i < list.getLength(); i++) {
193             consumer.accept(list.item(i));
194         }
195     }
196 
197     /**
198      * Merge the new document into the document that this reader processes.
199      *
200      * @param newDoc the Document to merge.
201      */
202     public void add(Document newDoc) {
203         nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.FAMILIES), nl -> nodeListConsumer(nl.getChildNodes(),
204                 n -> familiesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
205         nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.LICENSE),
206                 n -> licensesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
207         nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.APPROVED), nl -> nodeListConsumer(nl.getChildNodes(),
208                 n -> approvedElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
209         nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.MATCHERS),
210                 n -> matchersElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
211     }
212 
213     /**
214      * Get a map of Node attribute names to values.
215      *
216      * @param node The node to process
217      * @return the map of attributes on the node
218      */
219     private Map<String, String> attributes(Node node) {
220         NamedNodeMap nnm = node.getAttributes();
221         Map<String, String> result = new HashMap<>();
222         for (int i = 0; i < nnm.getLength(); i++) {
223             Node n = nnm.item(i);
224             result.put(n.getNodeName(), n.getNodeValue());
225         }
226         return result;
227     }
228 
229     private void callSetter(Description desc, IHeaderMatcher.Builder builder, Object value) {
230         try {
231             desc.setter(builder.getClass()).invoke(builder, value);
232         } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
233                 | SecurityException e) {
234             throw new ConfigurationException(e.getMessage(), e);
235         }
236     }
237 
238     private void processBuilderParams(Description description, IHeaderMatcher.Builder builder) {
239         for (Description desc : description.childrenOfType(ComponentType.BUILD_PARAMETER)) {
240             Method m = builderParams.get(desc.getCommonName());
241             try {
242                 callSetter(desc, builder, m.invoke(builderParams));
243             } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
244                 throw ImplementationException.makeInstance(e);
245             }
246         }
247     }
248 
249     /**
250      * Processes a list of children by passing each child node and the description
251      * of the child (if any) to the BiPredicate. If there is not a child description
252      * for the node it is ignored. If the node is processed it is removed from list
253      * of children.
254      * 
255      * @param description the Description of the node being processed
256      * @param children the child nodes of that node.
257      * @param childProcessor the function that handles the processing of the child
258      * node.
259      */
260     private void processChildren(Description description, List<Node> children,
261             BiPredicate<Node, Description> childProcessor) {
262         Iterator<Node> iter = children.iterator();
263         while (iter.hasNext()) {
264             Node child = iter.next();
265             Description childDescription = description.getChildren().get(child.getNodeName());
266             if (childDescription != null) {
267                 if (childProcessor.test(child, childDescription)) {
268                     iter.remove();
269                 }
270             }
271         }
272     }
273 
274     private BiPredicate<Node, Description> matcherChildNodeProcessor(AbstractBuilder builder, Description description) {
275         return (child, childDescription) -> {
276             switch (childDescription.getType()) {
277             case LICENSE:
278             case BUILD_PARAMETER:
279                 throw new ConfigurationException(String.format(
280                         "%s may not be used as an enclosed matcher.  %s '%s' found in '%s'", childDescription.getType(),
281                         childDescription.getType(), childDescription.getCommonName(), description.getCommonName()));
282             case MATCHER:
283                 AbstractBuilder b = parseMatcher(child);
284                 callSetter(b.getDescription(), builder, b);
285                 return true;
286             case PARAMETER:
287                 if (!XMLConfig.isInlineNode(description.getCommonName(), childDescription.getCommonName())
288                         || childDescription.getChildType() == String.class) {
289                     callSetter(childDescription, builder, child.getTextContent());
290                 } else {
291                     callSetter(childDescription, builder, parseMatcher(child));
292                 }
293                 return true;
294             }
295             return false;
296         };
297     }
298 
299     private void setValue(Description description, Description childDescription, IHeaderMatcher.Builder builder,
300             Node child) {
301         if (childDescription.getChildType() == String.class) {
302             callSetter(description, builder, child.getTextContent());
303         } else {
304             callSetter(description, builder, parseMatcher(child));
305         }
306     }
307 
308     /**
309      * Process the ELEEMENT_NODEs children of the parent whos names match child
310      * descriptions. All children children are processed with the childProcessor. If
311      * the childProcessor handles the node it is not included in the resulting list.
312      * 
313      * @param description the Description of the parent node.
314      * @param parent the node being processed
315      * @param childProcessor the BiProcessor to handle process each child. if the
316      * processor handles the child it must return {@code true}.
317      * @return A Pair comprising a boolean flag indicating children were found, and
318      * a list of all child nodes that were not processed by the childProcessor.
319      */
320     private Pair<Boolean, List<Node>> processChildNodes(Description description, Node parent,
321             BiPredicate<Node, Description> childProcessor) {
322         boolean foundChildren = false;
323         List<Node> children = new ArrayList<>();
324         // check XML child nodes.
325         if (parent.hasChildNodes()) {
326 
327             nodeListConsumer(parent.getChildNodes(), (n) -> {
328                 if (n.getNodeType() == Node.ELEMENT_NODE) {
329                     children.add(n);
330                 }
331             });
332             foundChildren = !children.isEmpty();
333             if (foundChildren) {
334                 processChildren(description, children, childProcessor);
335             }
336         }
337         return new ImmutablePair<>(foundChildren, children);
338     }
339 
340     private AbstractBuilder parseMatcher(Node matcherNode) {
341         final AbstractBuilder builder = MatcherBuilderTracker.getMatcherBuilder(matcherNode.getNodeName());
342 
343         try {
344             final Description description = DescriptionBuilder.buildMap(builder.builtClass());
345             if (description == null) {
346                 throw new ConfigurationException(String.format("Unable to build description for %s", builder.builtClass()));
347             }
348             processBuilderParams(description, builder);
349 
350             // process the attributes
351             description.setChildren(getLog(), builder, attributes(matcherNode));
352 
353             // check XML child nodes.
354             Pair<Boolean, List<Node>> pair = processChildNodes(description, matcherNode,
355                     matcherChildNodeProcessor(builder, description));
356             boolean foundChildren = pair.getLeft();
357             List<Node> children = pair.getRight();
358 
359             // check for inline nodes that can accept child nodes.
360             List<Description> childDescriptions = description.getChildren().values().stream()
361                     .filter(d -> XMLConfig.isInlineNode(description.getCommonName(), d.getCommonName()))
362                     .collect(Collectors.toList());
363 
364             for (Description childDescription : childDescriptions) {
365                 if (XMLConfig.isInlineNode(description.getCommonName(), childDescription.getCommonName())) {
366                     // can only process text inline if there were not child nodes.
367                     if (childDescription.getChildType() == String.class) {
368                         if (!foundChildren) {
369                             callSetter(childDescription, builder, matcherNode.getTextContent());
370                         }
371                     } else {
372                         Iterator<Node> iter = children.iterator();
373                         while (iter.hasNext()) {
374                             Node child = iter.next();
375                             callSetter(childDescription, builder, parseMatcher(child));
376                             iter.remove();
377                         }
378                     }
379 
380                 } else {
381                     processChildren(description, children, (child, childD) -> {
382                         if (childD.getChildType().equals(description.getChildType())) {
383                             setValue(childDescription, childD, builder, child);
384                             return true;
385                         }
386                         return false;
387                     });
388                 }
389 
390             }
391 
392             if (!children.isEmpty()) {
393                 children.forEach(n -> getLog().warn(String.format("unrecognised child node '%s' in node '%s'%n",
394                         n.getNodeName(), matcherNode.getNodeName())));
395             }
396 
397         } catch (DOMException e) {
398             throw new ConfigurationException(e);
399         }
400         return builder.hasId() ? new IDRecordingBuilder(matchers, builder) : builder;
401     }
402 
403     private BiPredicate<Node, Description> licenseChildNodeProcessor(ILicense.Builder builder,
404             Description description) {
405         return (child, childDescription) -> {
406             switch (childDescription.getType()) {
407             case LICENSE:
408                 throw new ConfigurationException(String.format(
409                         "%s may not be enclosed in another license.  %s '%s' found in '%s'", childDescription.getType(),
410                         childDescription.getType(), childDescription.getCommonName(), description.getCommonName()));
411             case BUILD_PARAMETER:
412                 break;
413             case MATCHER:
414                 AbstractBuilder b = parseMatcher(child);
415                 callSetter(b.getDescription(), builder, b);
416                 return true;
417             case PARAMETER:
418                 if ((!XMLConfig.isLicenseChild(childDescription.getCommonName()))
419                         || childDescription.getChildType() == String.class) {
420                     callSetter(childDescription, builder, child.getTextContent());
421                 } else {
422                     callSetter(childDescription, builder, parseMatcher(child));
423                 }
424                 return true;
425             }
426             return false;
427         };
428     }
429 
430     private ILicense parseLicense(Node licenseNode) {
431         ILicense.Builder builder = ILicense.builder();
432         // get the description for the builder
433         Description description = builder.getDescription();
434         // set the BUILDER_PARAM options from the description
435         processBuilderParams(description, builder);
436         // set the children from attributes.
437         description.setChildren(getLog(), builder, attributes(licenseNode));
438         // set children from the child nodes
439         Pair<Boolean, List<Node>> pair = processChildNodes(description, licenseNode,
440                 licenseChildNodeProcessor(builder, description));
441         List<Node> children = pair.getRight();
442 
443         // check for inline nodes that can accept child nodes.
444         List<Description> childDescriptions = description.getChildren().values().stream()
445                 .filter(d -> XMLConfig.isLicenseInline(d.getCommonName())).collect(Collectors.toList());
446         for (Description childDescription : childDescriptions) {
447             Iterator<Node> iter = children.iterator();
448             while (iter.hasNext()) {
449                 callSetter(childDescription, builder, parseMatcher(iter.next()));
450                 iter.remove();
451             }
452         }
453 
454         if (!children.isEmpty()) {
455             children.forEach(n -> getLog().warn(String.format("unrecognised child node '%s' in node '%s'%n",
456                     n.getNodeName(), licenseNode.getNodeName())));
457         }
458         return builder.build();
459     }
460 
461     @Override
462     public SortedSet<ILicense> readLicenses() {
463         readFamilies();
464         readMatcherBuilders();
465         if (licenses.isEmpty()) {
466             nodeListConsumer(document.getElementsByTagName(XMLConfig.LICENSE), x -> licenses.add(parseLicense(x)));
467             document = null;
468         }
469         return Collections.unmodifiableSortedSet(licenses);
470     }
471 
472     @Override
473     public SortedSet<ILicenseFamily> readFamilies() {
474         if (licenseFamilies.isEmpty()) {
475             nodeListConsumer(document.getElementsByTagName(XMLConfig.FAMILIES),
476                     x -> nodeListConsumer(x.getChildNodes(), this::parseFamily));
477             nodeListConsumer(document.getElementsByTagName(XMLConfig.APPROVED),
478                     x -> nodeListConsumer(x.getChildNodes(), this::parseApproved));
479         }
480         return Collections.unmodifiableSortedSet(licenseFamilies);
481     }
482 
483     private ILicenseFamily parseFamily(Map<String, String> attributes) {
484         if (attributes.containsKey(XMLConfig.ATT_ID)) {
485             ILicenseFamily.Builder builder = ILicenseFamily.builder();
486             builder.setLicenseFamilyCategory(attributes.get(XMLConfig.ATT_ID));
487             builder.setLicenseFamilyName(
488                     StringUtils.defaultIfBlank(attributes.get(XMLConfig.ATT_NAME), attributes.get(XMLConfig.ATT_ID)));
489             return builder.build();
490         }
491         return null;
492     }
493 
494     private void parseFamily(Node familyNode) {
495         if (XMLConfig.FAMILY.equals(familyNode.getNodeName())) {
496             ILicenseFamily result = parseFamily(attributes(familyNode));
497             if (result == null) {
498                 throw new ConfigurationException(
499                         String.format("families/family tag requires %s attribute", XMLConfig.ATT_ID));
500             }
501             licenseFamilies.add(result);
502         }
503     }
504 
505     private void parseApproved(Node approvedNode) {
506         if (XMLConfig.FAMILY.equals(approvedNode.getNodeName())) {
507             Map<String, String> attributes = attributes(approvedNode);
508             if (attributes.containsKey(XMLConfig.ATT_LICENSE_REF)) {
509                 approvedFamilies.add(attributes.get(XMLConfig.ATT_LICENSE_REF));
510             } else if (attributes.containsKey(XMLConfig.ATT_ID)) {
511                 ILicenseFamily target = parseFamily(attributes);
512                 licenseFamilies.add(target);
513                 approvedFamilies.add(target.getFamilyCategory());
514             } else {
515                 throw new ConfigurationException(String.format("family tag requires %s or %s attribute",
516                         XMLConfig.ATT_LICENSE_REF, XMLConfig.ATT_ID));
517             }
518         }
519     }
520 
521     ////////////////////////////////////////// MatcherReader methods
522     @Override
523     public SortedSet<String> approvedLicenseId() {
524         if (licenses.isEmpty()) {
525             this.readLicenses();
526         }
527         if (approvedFamilies.isEmpty()) {
528             SortedSet<String> result = new TreeSet<>();
529             licenses.stream().map(x -> x.getLicenseFamily().getFamilyCategory()).forEach(result::add);
530             return result;
531         }
532         return Collections.unmodifiableSortedSet(approvedFamilies);
533     }
534 
535     private void parseMatcherBuilder(Node classNode) {
536         Map<String, String> attributes = attributes(classNode);
537         if (attributes.get(XMLConfig.ATT_CLASS_NAME) == null) {
538             throw new ConfigurationException("matcher must have a " + XMLConfig.ATT_CLASS_NAME + " attribute");
539         }
540         MatcherBuilderTracker.addBuilder(attributes.get(XMLConfig.ATT_CLASS_NAME), attributes.get(XMLConfig.ATT_NAME));
541     }
542 
543     @Override
544     public void readMatcherBuilders() {
545         nodeListConsumer(document.getElementsByTagName(XMLConfig.MATCHER), this::parseMatcherBuilder);
546     }
547 
548     @Override
549     public void addMatchers(URL url) {
550         read(url);
551     }
552 
553     ////////////// Helper classes
554     /**
555      * An abstract builder that delegates to another abstract builder.
556      */
557     static class IDRecordingBuilder extends AbstractBuilder {
558         private final AbstractBuilder delegate;
559         private final Map<String, IHeaderMatcher> matchers;
560 
561         IDRecordingBuilder(final Map<String, IHeaderMatcher> matchers, final AbstractBuilder delegate) {
562             this.delegate = delegate;
563             this.matchers = matchers;
564             setId(delegate.getId());
565         }
566 
567         @Override
568         public IHeaderMatcher build() {
569             IHeaderMatcher result = delegate.build();
570             matchers.put(result.getId(), result);
571             return result;
572         }
573 
574         @Override
575         public Class<?> builtClass() throws SecurityException {
576             return delegate.builtClass();
577         }
578 
579         @Override
580         public Description getDescription() {
581             return delegate.getDescription();
582         }
583     }
584 }