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