blob: 728f8587f33b50c3b89a387fba4bd3efe5c90ae1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.rat.configuration;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.rat.BuilderParams;
import org.apache.rat.ConfigurationException;
import org.apache.rat.ImplementationException;
import org.apache.rat.analysis.IHeaderMatcher;
import org.apache.rat.config.parameters.ComponentType;
import org.apache.rat.config.parameters.Description;
import org.apache.rat.config.parameters.DescriptionBuilder;
import org.apache.rat.configuration.builders.AbstractBuilder;
import org.apache.rat.license.ILicense;
import org.apache.rat.license.ILicenseFamily;
import org.apache.rat.license.LicenseFamilySetFactory;
import org.apache.rat.license.LicenseSetFactory;
import org.apache.rat.utils.DefaultLog;
import org.apache.rat.utils.Log;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* A class that reads the XML configuration file format.
*/
public class XMLConfigurationReader implements LicenseReader, MatcherReader {
private Log log;
private Document document;
private final Element rootElement;
private final Element familiesElement;
private final Element licensesElement;
private final Element approvedElement;
private final Element matchersElement;
private final SortedSet<ILicense> licenses;
private final Map<String, IHeaderMatcher> matchers;
private final BuilderParams builderParams;
private final SortedSet<ILicenseFamily> licenseFamilies;
private final SortedSet<String> approvedFamilies;
/**
* Constructs the XML configuration reader.
*/
public XMLConfigurationReader() {
try {
document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
} catch (ParserConfigurationException e) {
throw new IllegalStateException("No XML parser defined", e);
}
rootElement = document.createElement(XMLConfig.ROOT);
document.appendChild(rootElement);
familiesElement = document.createElement(XMLConfig.FAMILIES);
rootElement.appendChild(familiesElement);
licensesElement = document.createElement(XMLConfig.LICENSES);
rootElement.appendChild(licensesElement);
approvedElement = document.createElement(XMLConfig.APPROVED);
rootElement.appendChild(approvedElement);
matchersElement = document.createElement(XMLConfig.MATCHERS);
rootElement.appendChild(matchersElement);
licenses = LicenseSetFactory.emptyLicenseSet();
licenseFamilies = LicenseFamilySetFactory.emptyLicenseFamilySet();
approvedFamilies = new TreeSet<>();
matchers = new HashMap<>();
builderParams = new BuilderParams() {
@Override
public Map<String, IHeaderMatcher> matcherMap() {
return matchers;
}
@Override
public SortedSet<ILicenseFamily> licenseFamilies() {
return licenseFamilies;
}
};
}
@Override
public void setLog(Log log) {
this.log = log;
}
public Log getLog() {
return log == null ? DefaultLog.INSTANCE : log;
}
@Override
public void addLicenses(URL url) {
read(url);
}
/**
* Read xml from a reader.
*
* @param reader the reader to read XML from.
*/
public void read(Reader reader) {
DocumentBuilder builder;
try {
builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new ConfigurationException("Unable to create DOM builder", e);
}
try {
add(builder.parse(new InputSource(reader)));
} catch (SAXException | IOException e) {
throw new ConfigurationException("Unable to read inputSource", e);
}
}
/**
* Read the urls and extract the DOM information to create new objects.
*
* @param urls The URLs to read.
*/
public void read(URL... urls) {
DocumentBuilder builder;
try {
builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new ConfigurationException("Unable to create DOM builder", e);
}
for (URL url : urls) {
try {
add(builder.parse(url.openStream()));
} catch (SAXException | IOException e) {
throw new ConfigurationException("Unable to read url: " + url, e);
}
}
}
/**
* Applies the {@code consumer} to each node in the {@code list}
*
* @param list the NodeList to process
* @param consumer the consumer to apply to each node in the list.
*/
private void nodeListConsumer(NodeList list, Consumer<Node> consumer) {
for (int i = 0; i < list.getLength(); i++) {
consumer.accept(list.item(i));
}
}
/**
* Merge the new document into the document that this reader processes.
*
* @param newDoc the Document to merge.
*/
public void add(Document newDoc) {
nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.FAMILIES), nl -> nodeListConsumer(nl.getChildNodes(),
n -> familiesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.LICENSE),
n -> licensesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.APPROVED), nl -> nodeListConsumer(nl.getChildNodes(),
n -> approvedElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.MATCHERS),
n -> matchersElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
}
/**
* Get a map of Node attribute names to values.
*
* @param node The node to process
* @return the map of attributes on the node
*/
private Map<String, String> attributes(Node node) {
NamedNodeMap nnm = node.getAttributes();
Map<String, String> result = new HashMap<>();
for (int i = 0; i < nnm.getLength(); i++) {
Node n = nnm.item(i);
result.put(n.getNodeName(), n.getNodeValue());
}
return result;
}
private void callSetter(Description desc, IHeaderMatcher.Builder builder, Object value) {
try {
desc.setter(builder.getClass()).invoke(builder, value);
} catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| SecurityException e) {
throw new ConfigurationException(e.getMessage(), e);
}
}
private void processBuilderParams(Description description, IHeaderMatcher.Builder builder) {
for (Description desc : description.childrenOfType(ComponentType.BUILD_PARAMETER)) {
Method m = builderParams.get(desc.getCommonName());
try {
callSetter(desc, builder, m.invoke(builderParams));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw ImplementationException.makeInstance(e);
}
}
}
/**
* Processes a list of children by passing each child node and the description
* of the child (if any) to the BiPredicate. If there is not a child description
* for the node it is ignored. If the node is processed it is removed from list
* of children.
*
* @param description the Description of the node being processed
* @param children the child nodes of that node.
* @param childProcessor the function that handles the processing of the child
* node.
*/
private void processChildren(Description description, List<Node> children,
BiPredicate<Node, Description> childProcessor) {
Iterator<Node> iter = children.iterator();
while (iter.hasNext()) {
Node child = iter.next();
Description childDescription = description.getChildren().get(child.getNodeName());
if (childDescription != null) {
if (childProcessor.test(child, childDescription)) {
iter.remove();
}
}
}
}
private BiPredicate<Node, Description> matcherChildNodeProcessor(AbstractBuilder builder, Description description) {
return (child, childDescription) -> {
switch (childDescription.getType()) {
case LICENSE:
case BUILD_PARAMETER:
throw new ConfigurationException(String.format(
"%s may not be used as an enclosed matcher. %s '%s' found in '%s'", childDescription.getType(),
childDescription.getType(), childDescription.getCommonName(), description.getCommonName()));
case MATCHER:
AbstractBuilder b = parseMatcher(child);
callSetter(b.getDescription(), builder, b);
return true;
case PARAMETER:
if (!XMLConfig.isInlineNode(description.getCommonName(), childDescription.getCommonName())
|| childDescription.getChildType() == String.class) {
callSetter(childDescription, builder, child.getTextContent());
} else {
callSetter(childDescription, builder, parseMatcher(child));
}
return true;
}
return false;
};
}
private void setValue(Description description, Description childDescription, IHeaderMatcher.Builder builder,
Node child) {
if (childDescription.getChildType() == String.class) {
callSetter(description, builder, child.getTextContent());
} else {
callSetter(description, builder, parseMatcher(child));
}
}
/**
* Process the ELEEMENT_NODEs children of the parent whos names match child
* descriptions. All children children are processed with the childProcessor. If
* the childProcessor handles the node it is not included in the resulting list.
*
* @param description the Description of the parent node.
* @param parent the node being processed
* @param childProcessor the BiProcessor to handle process each child. if the
* processor handles the child it must return {@code true}.
* @return A Pair comprising a boolean flag indicating children were found, and
* a list of all child nodes that were not processed by the childProcessor.
*/
private Pair<Boolean, List<Node>> processChildNodes(Description description, Node parent,
BiPredicate<Node, Description> childProcessor) {
boolean foundChildren = false;
List<Node> children = new ArrayList<>();
// check XML child nodes.
if (parent.hasChildNodes()) {
nodeListConsumer(parent.getChildNodes(), (n) -> {
if (n.getNodeType() == Node.ELEMENT_NODE) {
children.add(n);
}
});
foundChildren = !children.isEmpty();
if (foundChildren) {
processChildren(description, children, childProcessor);
}
}
return new ImmutablePair<Boolean, List<Node>>(foundChildren, children);
}
private AbstractBuilder parseMatcher(Node matcherNode) {
final AbstractBuilder builder = MatcherBuilderTracker.getMatcherBuilder(matcherNode.getNodeName());
if (builder == null) {
throw new ConfigurationException(String.format("No builder found for: %s", matcherNode.getNodeName()));
}
try {
final Description description = DescriptionBuilder.buildMap(builder.builtClass());
processBuilderParams(description, builder);
// for (Description desc : description.childrenOfType(Component.Type.BuilderParam)) {
// Method m = builderParams.get(desc.getCommonName());
// try {
// callSetter(desc, builder, m.invoke(builderParams));
// } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
// ImplementationException.makeInstance(e);
// }
// }
// process the attributes
description.setChildren(getLog(), builder, attributes(matcherNode));
// check XML child nodes.
Pair<Boolean, List<Node>> pair = processChildNodes(description, matcherNode,
matcherChildNodeProcessor(builder, description));
boolean foundChildren = pair.getLeft();
List<Node> children = pair.getRight();
// check for inline nodes that can accept child nodes.
List<Description> childDescriptions = description.getChildren().values().stream()
.filter(d -> XMLConfig.isInlineNode(description.getCommonName(), d.getCommonName()))
.collect(Collectors.toList());
for (Description childDescription : childDescriptions) {
if (XMLConfig.isInlineNode(description.getCommonName(), childDescription.getCommonName())) {
// can only process text inline if there were not child nodes.
if (childDescription.getChildType() == String.class) {
if (!foundChildren) {
callSetter(childDescription, builder, matcherNode.getTextContent());
}
} else {
Iterator<Node> iter = children.iterator();
while (iter.hasNext()) {
Node child = iter.next();
callSetter(childDescription, builder, parseMatcher(child));
iter.remove();
}
}
} else {
processChildren(description, children, (child, childD) -> {
if (childD.getChildType().equals(description.getChildType())) {
setValue(childDescription, childD, builder, child);
return true;
}
return false;
});
}
}
if (!children.isEmpty()) {
children.forEach(n -> getLog().warn(String.format("unrecognised child node '%s' in node '%s'%n",
n.getNodeName(), matcherNode.getNodeName())));
}
} catch (DOMException e) {
throw new ConfigurationException(e);
}
return builder.hasId() ? new IDRecordingBuilder(matchers, builder) : builder;
}
private BiPredicate<Node, Description> licenseChildNodeProcessor(ILicense.Builder builder,
Description description) {
return (child, childDescription) -> {
switch (childDescription.getType()) {
case LICENSE:
throw new ConfigurationException(String.format(
"%s may not be enclosed in another license. %s '%s' found in '%s'", childDescription.getType(),
childDescription.getType(), childDescription.getCommonName(), description.getCommonName()));
case BUILD_PARAMETER:
break;
case MATCHER:
AbstractBuilder b = parseMatcher(child);
callSetter(b.getDescription(), builder, b);
return true;
case PARAMETER:
if ((!XMLConfig.isLicenseChild(childDescription.getCommonName()))
|| childDescription.getChildType() == String.class) {
callSetter(childDescription, builder, child.getTextContent());
} else {
callSetter(childDescription, builder, parseMatcher(child));
}
return true;
}
return false;
};
}
private ILicense parseLicense(Node licenseNode) {
ILicense.Builder builder = ILicense.builder();
Description description = builder.getDescription();
processBuilderParams(description, builder);
description.setChildren(getLog(), builder, attributes(licenseNode));
Pair<Boolean, List<Node>> pair = processChildNodes(description, licenseNode,
licenseChildNodeProcessor(builder, description));
List<Node> children = pair.getRight();
// check for inline nodes that can accept child nodes.
List<Description> childDescriptions = description.getChildren().values().stream()
.filter(d -> XMLConfig.isLicenseInline(d.getCommonName())).collect(Collectors.toList());
for (Description childDescription : childDescriptions) {
Iterator<Node> iter = children.iterator();
while (iter.hasNext()) {
Node child = iter.next();
if (MatcherBuilderTracker.getMatcherBuilder(child.getNodeName()) != null) {
callSetter(childDescription, builder, parseMatcher(child));
iter.remove();
}
}
}
if (!children.isEmpty()) {
children.forEach(n -> getLog().warn(String.format("unrecognised child node '%s' in node '%s'%n",
n.getNodeName(), licenseNode.getNodeName())));
}
return builder.build();
}
@Override
public SortedSet<ILicense> readLicenses() {
readFamilies();
readMatcherBuilders();
if (licenses.isEmpty()) {
nodeListConsumer(document.getElementsByTagName(XMLConfig.LICENSE), x -> licenses.add(parseLicense(x)));
document = null;
}
return Collections.unmodifiableSortedSet(licenses);
}
@Override
public SortedSet<ILicenseFamily> readFamilies() {
if (licenseFamilies.isEmpty()) {
nodeListConsumer(document.getElementsByTagName(XMLConfig.FAMILIES),
x -> nodeListConsumer(x.getChildNodes(), this::parseFamily));
nodeListConsumer(document.getElementsByTagName(XMLConfig.APPROVED),
x -> nodeListConsumer(x.getChildNodes(), this::parseApproved));
}
return Collections.unmodifiableSortedSet(licenseFamilies);
}
private ILicenseFamily parseFamily(Map<String, String> attributes) {
if (attributes.containsKey(XMLConfig.ATT_ID)) {
ILicenseFamily.Builder builder = ILicenseFamily.builder();
builder.setLicenseFamilyCategory(attributes.get(XMLConfig.ATT_ID));
builder.setLicenseFamilyName(
StringUtils.defaultIfBlank(attributes.get(XMLConfig.ATT_NAME), attributes.get(XMLConfig.ATT_ID)));
return builder.build();
}
return null;
}
private void parseFamily(Node familyNode) {
if (XMLConfig.FAMILY.equals(familyNode.getNodeName())) {
ILicenseFamily result = parseFamily(attributes(familyNode));
if (result == null) {
throw new ConfigurationException(
String.format("families/family tag requires %s attribute", XMLConfig.ATT_ID));
}
licenseFamilies.add(result);
}
}
private void parseApproved(Node approvedNode) {
if (XMLConfig.FAMILY.equals(approvedNode.getNodeName())) {
Map<String, String> attributes = attributes(approvedNode);
if (attributes.containsKey(XMLConfig.ATT_LICENSE_REF)) {
approvedFamilies.add(attributes.get(XMLConfig.ATT_LICENSE_REF));
} else if (attributes.containsKey(XMLConfig.ATT_ID)) {
ILicenseFamily target = parseFamily(attributes);
licenseFamilies.add(target);
approvedFamilies.add(target.getFamilyCategory());
} else {
throw new ConfigurationException(String.format("family tag requires %s or %s attribute",
XMLConfig.ATT_LICENSE_REF, XMLConfig.ATT_ID));
}
}
}
////////////////////////////////////////// MatcherReader methods
@Override
public SortedSet<String> approvedLicenseId() {
if (licenses.isEmpty()) {
this.readLicenses();
}
if (approvedFamilies.isEmpty()) {
SortedSet<String> result = new TreeSet<>();
licenses.stream().map(x -> x.getLicenseFamily().getFamilyCategory()).forEach(result::add);
return result;
}
return Collections.unmodifiableSortedSet(approvedFamilies);
}
private void parseMatcherBuilder(Node classNode) {
Map<String, String> attributes = attributes(classNode);
if (attributes.get(XMLConfig.ATT_CLASS_NAME) == null) {
throw new ConfigurationException("matcher must have a " + XMLConfig.ATT_CLASS_NAME + " attribute");
}
MatcherBuilderTracker.addBuilder(attributes.get(XMLConfig.ATT_CLASS_NAME), attributes.get(XMLConfig.ATT_NAME));
}
@Override
public void readMatcherBuilders() {
nodeListConsumer(document.getElementsByTagName(XMLConfig.MATCHER), this::parseMatcherBuilder);
}
@Override
public void addMatchers(URL url) {
read(url);
}
////////////// Helper classes
/**
* An abstract builder that delegates to another abstract builder.
*/
static class IDRecordingBuilder extends AbstractBuilder {
private final AbstractBuilder delegate;
private final Map<String, IHeaderMatcher> matchers;
IDRecordingBuilder(final Map<String, IHeaderMatcher> matchers, final AbstractBuilder delegate) {
this.delegate = delegate;
this.matchers = matchers;
setId(delegate.getId());
}
@Override
public IHeaderMatcher build() {
IHeaderMatcher result = delegate.build();
matchers.put(result.getId(), result);
return result;
}
@Override
public Class<?> builtClass() throws SecurityException {
return delegate.builtClass();
}
@Override
public Description getDescription() {
return delegate.getDescription();
}
}
}