1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.URI;
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
38 import javax.xml.parsers.DocumentBuilder;
39
40 import org.apache.commons.lang3.StringUtils;
41 import org.apache.commons.lang3.tuple.ImmutablePair;
42 import org.apache.commons.lang3.tuple.Pair;
43 import org.apache.rat.BuilderParams;
44 import org.apache.rat.ConfigurationException;
45 import org.apache.rat.ImplementationException;
46 import org.apache.rat.analysis.IHeaderMatcher;
47 import org.apache.rat.config.parameters.ComponentType;
48 import org.apache.rat.config.parameters.Description;
49 import org.apache.rat.config.parameters.DescriptionBuilder;
50 import org.apache.rat.configuration.builders.AbstractBuilder;
51 import org.apache.rat.license.ILicense;
52 import org.apache.rat.license.ILicenseFamily;
53 import org.apache.rat.utils.DefaultLog;
54 import org.apache.rat.utils.StandardXmlFactory;
55 import org.w3c.dom.DOMException;
56 import org.w3c.dom.Document;
57 import org.w3c.dom.Element;
58 import org.w3c.dom.NamedNodeMap;
59 import org.w3c.dom.Node;
60 import org.w3c.dom.NodeList;
61 import org.xml.sax.InputSource;
62 import org.xml.sax.SAXException;
63
64
65
66
67 public final class XMLConfigurationReader implements LicenseReader, MatcherReader {
68
69 private Document document;
70
71 private final Element rootElement;
72
73 private final Element familiesElement;
74
75 private final Element licensesElement;
76
77 private final Element approvedElement;
78
79 private final Element matchersElement;
80
81 private final SortedSet<ILicense> licenses;
82
83 private final Map<String, IHeaderMatcher> matchers;
84
85 private final BuilderParams builderParams;
86
87 private final SortedSet<ILicenseFamily> licenseFamilies;
88
89 private final SortedSet<String> approvedFamilies;
90
91
92
93
94 public XMLConfigurationReader() {
95 document = StandardXmlFactory.documentBuilder().newDocument();
96 rootElement = document.createElement(XMLConfig.ROOT);
97 document.appendChild(rootElement);
98 familiesElement = document.createElement(XMLConfig.FAMILIES);
99 rootElement.appendChild(familiesElement);
100 licensesElement = document.createElement(XMLConfig.LICENSES);
101 rootElement.appendChild(licensesElement);
102 approvedElement = document.createElement(XMLConfig.APPROVED);
103 rootElement.appendChild(approvedElement);
104 matchersElement = document.createElement(XMLConfig.MATCHERS);
105 rootElement.appendChild(matchersElement);
106 licenses = new TreeSet<>();
107 licenseFamilies = new TreeSet<>();
108 approvedFamilies = new TreeSet<>();
109 matchers = new HashMap<>();
110 builderParams = new BuilderParams() {
111 @Override
112 public Map<String, IHeaderMatcher> matcherMap() {
113 return matchers;
114 }
115
116 @Override
117 public SortedSet<ILicenseFamily> licenseFamilies() {
118 return licenseFamilies;
119 }
120 };
121 }
122
123
124
125
126
127
128 private String nodeText(final Node node) {
129 StringBuilder stringBuilder = new StringBuilder().append("<").append(node.getNodeName());
130 NamedNodeMap attr = node.getAttributes();
131 for (int i = 0; i < attr.getLength(); i++) {
132 Node n = attr.item(i);
133 stringBuilder.append(String.format(" %s='%s'", n.getNodeName(), n.getNodeValue()));
134 }
135 return stringBuilder.append(">").toString();
136 }
137
138 @Override
139 public void addLicenses(final URI uri) {
140 read(uri);
141 }
142
143
144
145
146
147 public void read(final Reader reader) {
148 try {
149 add(StandardXmlFactory.documentBuilder().parse(new InputSource(reader)));
150 } catch (SAXException | IOException e) {
151 throw new ConfigurationException("Unable to read inputSource", e);
152 }
153 }
154
155
156
157
158
159 public void read(final URI... uris) {
160 DocumentBuilder builder = StandardXmlFactory.documentBuilder();
161 for (URI uri : uris) {
162 try (InputStream inputStream = uri.toURL().openStream()) {
163 add(builder.parse(inputStream));
164 } catch (SAXException | IOException e) {
165 throw new ConfigurationException("Unable to read uri: " + uri, e);
166 }
167 }
168 }
169
170
171
172
173
174
175
176 public static void nodeListConsumer(final NodeList list, final Consumer<Node> consumer) {
177 for (int i = 0; i < list.getLength(); i++) {
178 consumer.accept(list.item(i));
179 }
180 }
181
182
183
184
185
186 public void add(final Document newDoc) {
187 nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.FAMILIES), nl -> nodeListConsumer(nl.getChildNodes(),
188 n -> familiesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
189 nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.LICENSE),
190 n -> licensesElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
191 nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.APPROVED), nl -> nodeListConsumer(nl.getChildNodes(),
192 n -> approvedElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true)))));
193 nodeListConsumer(newDoc.getElementsByTagName(XMLConfig.MATCHERS),
194 n -> matchersElement.appendChild(rootElement.getOwnerDocument().adoptNode(n.cloneNode(true))));
195 }
196
197
198
199
200
201
202 public static Map<String, String> attributes(final Node node) {
203 NamedNodeMap nnm = node.getAttributes();
204 Map<String, String> result = new HashMap<>();
205 for (int i = 0; i < nnm.getLength(); i++) {
206 Node n = nnm.item(i);
207 result.put(n.getNodeName(), n.getNodeValue());
208 }
209 return result;
210 }
211
212
213
214
215
216
217
218 private void callSetter(final Description desc, final IHeaderMatcher.Builder builder, final Object value) {
219 try {
220 desc.setter(builder.getClass()).invoke(builder, value);
221 } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
222 | SecurityException e) {
223 throw new ConfigurationException(e.getMessage(), e);
224 }
225 }
226
227
228
229
230
231
232 private void processBuilderParams(final Description description, final IHeaderMatcher.Builder builder) {
233 for (Description desc : description.childrenOfType(ComponentType.BUILD_PARAMETER)) {
234 Method m = builderParams.get(desc.getCommonName());
235 try {
236 callSetter(desc, builder, m.invoke(builderParams));
237 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
238 throw ImplementationException.makeInstance(e);
239 }
240 }
241 }
242
243
244
245
246
247
248
249
250
251
252
253 private void processChildren(final Description description, final List<Node> children,
254 final BiPredicate<Node, Description> childProcessor) {
255 Iterator<Node> iter = children.iterator();
256 while (iter.hasNext()) {
257 Node child = iter.next();
258 Description childDescription = description.getChildren().get(child.getNodeName());
259 if (childDescription != null) {
260 if (childProcessor.test(child, childDescription)) {
261 iter.remove();
262 }
263 }
264 }
265 }
266
267
268
269
270
271
272
273 private BiPredicate<Node, Description> matcherChildNodeProcessor(final AbstractBuilder builder, final Description description) {
274 return (child, childDescription) -> {
275 switch (childDescription.getType()) {
276 case LICENSE:
277 case BUILD_PARAMETER:
278 throw new ConfigurationException(String.format(
279 "%s may not be used as an enclosed matcher. %s '%s' found in '%s'", childDescription.getType(),
280 childDescription.getType(), childDescription.getCommonName(), description.getCommonName()));
281 case MATCHER:
282 AbstractBuilder b = parseMatcher(child);
283 callSetter(b.getDescription(), builder, b);
284 return true;
285 case PARAMETER:
286 if (!XMLConfig.isInlineNode(description.getCommonName(), childDescription.getCommonName())
287 || childDescription.getChildType() == String.class) {
288 callSetter(childDescription, builder, child.getTextContent());
289 } else {
290 callSetter(childDescription, builder, parseMatcher(child));
291 }
292 return true;
293 }
294 return false;
295 };
296 }
297
298
299
300
301
302
303
304
305 private void setValue(final Description description, final Description childDescription, final IHeaderMatcher.Builder builder,
306 final Node child) {
307 if (childDescription.getChildType() == String.class) {
308 callSetter(description, builder, child.getTextContent());
309 } else {
310 callSetter(description, builder, parseMatcher(child));
311 }
312 }
313
314
315
316
317
318
319
320
321
322
323
324
325 private Pair<Boolean, List<Node>> processChildNodes(final Description description, final Node parent,
326 final BiPredicate<Node, Description> childProcessor) {
327 try {
328 boolean foundChildren = false;
329 List<Node> children = new ArrayList<>();
330
331 if (parent.hasChildNodes()) {
332
333 nodeListConsumer(parent.getChildNodes(), n -> {
334 if (n.getNodeType() == Node.ELEMENT_NODE) {
335 children.add(n);
336 }
337 });
338 foundChildren = !children.isEmpty();
339 if (foundChildren) {
340 processChildren(description, children, childProcessor);
341 }
342 }
343 return new ImmutablePair<>(foundChildren, children);
344 } catch (RuntimeException exception) {
345 DefaultLog.getInstance().error(String.format("Child node extraction error in: '%s'", nodeText(parent)));
346 throw exception;
347 }
348 }
349
350
351
352
353
354
355 private AbstractBuilder parseMatcher(final Node matcherNode) {
356 final AbstractBuilder builder = MatcherBuilderTracker.getMatcherBuilder(matcherNode.getNodeName());
357
358 try {
359 final Description description = DescriptionBuilder.buildMap(builder.getClass());
360 if (description == null) {
361 throw new ConfigurationException(String.format("Unable to build description for %s", builder.getClass()));
362 }
363 processBuilderParams(description, builder);
364
365
366 description.setChildren(builder, attributes(matcherNode));
367
368
369 Pair<Boolean, List<Node>> pair = processChildNodes(description, matcherNode,
370 matcherChildNodeProcessor(builder, description));
371 boolean foundChildren = pair.getLeft();
372 List<Node> children = pair.getRight();
373
374
375 List<Description> childDescriptions = description.getChildren().values().stream()
376 .filter(d -> XMLConfig.isInlineNode(description.getCommonName(), d.getCommonName()))
377 .toList();
378
379 for (Description childDescription : childDescriptions) {
380 if (XMLConfig.isInlineNode(description.getCommonName(), childDescription.getCommonName())) {
381
382 if (childDescription.getChildType() == String.class) {
383 if (!foundChildren) {
384 callSetter(childDescription, builder, matcherNode.getTextContent());
385 }
386 } else {
387 Iterator<Node> iter = children.iterator();
388 while (iter.hasNext()) {
389 Node child = iter.next();
390 callSetter(childDescription, builder, parseMatcher(child));
391 iter.remove();
392 }
393 }
394 } else {
395 processChildren(description, children, (child, childD) -> {
396 if (childD.getChildType().equals(description.getChildType())) {
397 setValue(childDescription, childD, builder, child);
398 return true;
399 }
400 return false;
401 });
402 }
403 }
404
405 if (!children.isEmpty()) {
406 children.forEach(n -> DefaultLog.getInstance().warn(String.format("unrecognised child node '%s' in node '%s'%n",
407 n.getNodeName(), matcherNode.getNodeName())));
408 }
409
410 } catch (DOMException e) {
411 DefaultLog.getInstance().error(String.format("Matcher error in: '%s'", nodeText(matcherNode)));
412 throw new ConfigurationException(e);
413 }
414 return builder.hasId() ? new IDRecordingBuilder(matchers, builder) : builder;
415 }
416
417 private BiPredicate<Node, Description> licenseChildNodeProcessor(final ILicense.Builder builder, final Description description) {
418 return (child, childDescription) -> {
419 switch (childDescription.getType()) {
420 case LICENSE:
421 throw new ConfigurationException(String.format(
422 "%s may not be enclosed in another license. %s '%s' found in '%s'", childDescription.getType(),
423 childDescription.getType(), childDescription.getCommonName(), description.getCommonName()));
424 case BUILD_PARAMETER:
425 break;
426 case MATCHER:
427 AbstractBuilder b = parseMatcher(child);
428 callSetter(b.getDescription(), builder, b);
429 return true;
430 case PARAMETER:
431 if (!XMLConfig.isLicenseChild(childDescription.getCommonName())
432 || childDescription.getChildType() == String.class) {
433 callSetter(childDescription, builder, child.getTextContent());
434 } else {
435 callSetter(childDescription, builder, parseMatcher(child));
436 }
437 return true;
438 }
439 return false;
440 };
441 }
442
443
444
445
446
447
448 private ILicense parseLicense(final Node licenseNode) {
449 try {
450 ILicense.Builder builder = ILicense.builder();
451
452 Description description = builder.getDescription();
453
454 processBuilderParams(description, builder);
455
456 description.setChildren(builder, attributes(licenseNode));
457
458 Pair<Boolean, List<Node>> pair = processChildNodes(description, licenseNode,
459 licenseChildNodeProcessor(builder, description));
460 List<Node> children = pair.getRight();
461
462
463 List<Description> childDescriptions = description.getChildren().values().stream()
464 .filter(d -> XMLConfig.isLicenseInline(d.getCommonName())).toList();
465 for (Description childDescription : childDescriptions) {
466 Iterator<Node> iter = children.iterator();
467 while (iter.hasNext()) {
468 callSetter(childDescription, builder, parseMatcher(iter.next()));
469 iter.remove();
470 }
471 }
472
473 if (!children.isEmpty()) {
474 children.forEach(n -> DefaultLog.getInstance().warn(String.format("unrecognised child node '%s' in node '%s'%n",
475 n.getNodeName(), licenseNode.getNodeName())));
476 }
477 return builder.build();
478 } catch (RuntimeException exception) {
479 DefaultLog.getInstance().error(String.format("License error in: '%s'", nodeText(licenseNode)));
480 throw exception;
481 }
482 }
483
484 @Override
485 public SortedSet<ILicense> readLicenses() {
486 if (this.document != null) {
487 readFamilies();
488 readMatcherBuilders();
489 if (licenses.isEmpty()) {
490 nodeListConsumer(document.getElementsByTagName(XMLConfig.LICENSE), x -> licenses.add(parseLicense(x)));
491 document = null;
492 }
493 }
494 return Collections.unmodifiableSortedSet(licenses);
495 }
496
497 @Override
498 public SortedSet<ILicenseFamily> readFamilies() {
499 if (licenseFamilies.isEmpty()) {
500 nodeListConsumer(document.getElementsByTagName(XMLConfig.FAMILIES),
501 x -> nodeListConsumer(x.getChildNodes(), this::parseFamily));
502 nodeListConsumer(document.getElementsByTagName(XMLConfig.APPROVED),
503 x -> nodeListConsumer(x.getChildNodes(), this::parseApproved));
504 }
505 return Collections.unmodifiableSortedSet(licenseFamilies);
506 }
507
508
509
510
511
512
513 private ILicenseFamily parseFamily(final Map<String, String> attributes) {
514 if (attributes.containsKey(XMLConfig.ATT_ID)) {
515 ILicenseFamily.Builder builder = ILicenseFamily.builder();
516 builder.setLicenseFamilyCategory(attributes.get(XMLConfig.ATT_ID));
517 builder.setLicenseFamilyName(
518 StringUtils.defaultIfBlank(attributes.get(XMLConfig.ATT_NAME), attributes.get(XMLConfig.ATT_ID)));
519 return builder.build();
520 }
521 return null;
522 }
523
524
525
526
527
528 private void parseFamily(final Node familyNode) {
529 if (XMLConfig.FAMILY.equals(familyNode.getNodeName())) {
530 try {
531 ILicenseFamily result = parseFamily(attributes(familyNode));
532 if (result == null) {
533 throw new ConfigurationException(
534 String.format("families/family tag requires %s attribute", XMLConfig.ATT_ID));
535 }
536 licenseFamilies.add(result);
537 } catch (RuntimeException exception) {
538 DefaultLog.getInstance().error(String.format("Family error in: '%s'", nodeText(familyNode)));
539 throw exception;
540 }
541 }
542 }
543
544
545
546
547
548
549 private void parseApproved(final Node approvedNode) {
550 if (XMLConfig.FAMILY.equals(approvedNode.getNodeName())) {
551 try {
552 Map<String, String> attributes = attributes(approvedNode);
553 if (attributes.containsKey(XMLConfig.ATT_LICENSE_REF)) {
554 approvedFamilies.add(attributes.get(XMLConfig.ATT_LICENSE_REF));
555 } else if (attributes.containsKey(XMLConfig.ATT_ID)) {
556 ILicenseFamily target = parseFamily(attributes);
557 if (target != null) {
558 licenseFamilies.add(target);
559 String familyCategory = target.getFamilyCategory();
560 if (StringUtils.isNotBlank(familyCategory)) {
561 approvedFamilies.add(familyCategory);
562 }
563 }
564 } else {
565 throw new ConfigurationException(String.format("family tag requires %s or %s attribute",
566 XMLConfig.ATT_LICENSE_REF, XMLConfig.ATT_ID));
567 }
568 } catch (RuntimeException exception) {
569 DefaultLog.getInstance().error(String.format("Approved error in: '%s'", nodeText(approvedNode)));
570 throw exception;
571 }
572 }
573 }
574
575
576 @Override
577 public SortedSet<String> approvedLicenseId() {
578 if (licenses.isEmpty()) {
579 this.readLicenses();
580 }
581 if (approvedFamilies.isEmpty()) {
582 SortedSet<String> result = new TreeSet<>();
583 licenses.stream().map(x -> x.getLicenseFamily().getFamilyCategory()).forEach(result::add);
584 return result;
585 }
586 return Collections.unmodifiableSortedSet(approvedFamilies);
587 }
588
589 private void parseMatcherBuilder(final Node classNode) {
590 try {
591 Map<String, String> attributes = attributes(classNode);
592 if (attributes.get(XMLConfig.ATT_CLASS_NAME) == null) {
593 throw new ConfigurationException("matcher must have a " + XMLConfig.ATT_CLASS_NAME + " attribute");
594 }
595 MatcherBuilderTracker.addBuilder(attributes.get(XMLConfig.ATT_CLASS_NAME), attributes.get(XMLConfig.ATT_NAME));
596 } catch (RuntimeException exception) {
597 DefaultLog.getInstance().error(String.format("Matcher error in: '%s'", nodeText(classNode)));
598 throw exception;
599 }
600 }
601
602 @Override
603 public void readMatcherBuilders() {
604 nodeListConsumer(document.getElementsByTagName(XMLConfig.MATCHER), this::parseMatcherBuilder);
605 }
606
607 @Override
608 public void addMatchers(final URI uri) {
609 read(uri);
610 }
611
612
613
614
615 static class IDRecordingBuilder extends AbstractBuilder {
616
617 private final AbstractBuilder delegate;
618
619
620
621
622 private final Map<String, IHeaderMatcher> matchers;
623
624 IDRecordingBuilder(final Map<String, IHeaderMatcher> matchers, final AbstractBuilder delegate) {
625 this.delegate = delegate;
626 this.matchers = matchers;
627 setId(delegate.getId());
628 }
629
630 @Override
631 public IHeaderMatcher build() {
632 IHeaderMatcher result = delegate.build();
633 matchers.put(result.getId(), result);
634 return result;
635 }
636
637 @Override
638 public Description getDescription() {
639 return delegate.getDescription();
640 }
641 }
642 }