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         readFamilies();
507         readMatcherBuilders();
508         if (licenses.isEmpty()) {
509             nodeListConsumer(document.getElementsByTagName(XMLConfig.LICENSE), x -> licenses.add(parseLicense(x)));
510             document = null;
511         }
512         return Collections.unmodifiableSortedSet(licenses);
513     }
514 
515     @Override
516     public SortedSet<ILicenseFamily> readFamilies() {
517         if (licenseFamilies.isEmpty()) {
518             nodeListConsumer(document.getElementsByTagName(XMLConfig.FAMILIES),
519                     x -> nodeListConsumer(x.getChildNodes(), this::parseFamily));
520             nodeListConsumer(document.getElementsByTagName(XMLConfig.APPROVED),
521                     x -> nodeListConsumer(x.getChildNodes(), this::parseApproved));
522         }
523         return Collections.unmodifiableSortedSet(licenseFamilies);
524     }
525 
526     /**
527      * Parses a license family from a map that contains the ID and Name attributes.
528      * @param attributes the map of attributes.
529      * @return the license family defined in the map.
530      */
531     private ILicenseFamily parseFamily(final Map<String, String> attributes) {
532         if (attributes.containsKey(XMLConfig.ATT_ID)) {
533             ILicenseFamily.Builder builder = ILicenseFamily.builder();
534             builder.setLicenseFamilyCategory(attributes.get(XMLConfig.ATT_ID));
535             builder.setLicenseFamilyName(
536                     StringUtils.defaultIfBlank(attributes.get(XMLConfig.ATT_NAME), attributes.get(XMLConfig.ATT_ID)));
537             return builder.build();
538         }
539         return null;
540     }
541 
542     /**
543      * Parses a license family node into a license family and adds it to the license families set.
544      * @param familyNode the node to parse.
545      */
546     private void parseFamily(final Node familyNode) {
547         if (XMLConfig.FAMILY.equals(familyNode.getNodeName())) {
548             try {
549                 ILicenseFamily result = parseFamily(attributes(familyNode));
550                 if (result == null) {
551                     throw new ConfigurationException(
552                             String.format("families/family tag requires %s attribute", XMLConfig.ATT_ID));
553                 }
554                 licenseFamilies.add(result);
555             } catch (RuntimeException exception) {
556                 DefaultLog.getInstance().error(String.format("Family error in: '%s'", nodeText(familyNode)));
557                 throw exception;
558             }
559         }
560     }
561 
562     /**
563      * Parse an approved License family and adds it to the set of license families as well as the
564      * set of approved license families.
565      * @param approvedNode the node to parse.
566      */
567     private void parseApproved(final Node approvedNode) {
568         if (XMLConfig.FAMILY.equals(approvedNode.getNodeName())) {
569             try {
570                 Map<String, String> attributes = attributes(approvedNode);
571                 if (attributes.containsKey(XMLConfig.ATT_LICENSE_REF)) {
572                     approvedFamilies.add(attributes.get(XMLConfig.ATT_LICENSE_REF));
573                 } else if (attributes.containsKey(XMLConfig.ATT_ID)) {
574                     ILicenseFamily target = parseFamily(attributes);
575                     if (target != null) {
576                         licenseFamilies.add(target);
577                         String familyCategory = target.getFamilyCategory();
578                         if (StringUtils.isNotBlank(familyCategory)) {
579                             approvedFamilies.add(familyCategory);
580                         }
581                     }
582                 } else {
583                     throw new ConfigurationException(String.format("family tag requires %s or %s attribute",
584                             XMLConfig.ATT_LICENSE_REF, XMLConfig.ATT_ID));
585                 }
586             } catch (RuntimeException exception) {
587                 DefaultLog.getInstance().error(String.format("Approved error in: '%s'", nodeText(approvedNode)));
588                 throw exception;
589             }
590         }
591     }
592 
593     ////////////////////////////////////////// MatcherReader methods
594     @Override
595     public SortedSet<String> approvedLicenseId() {
596         if (licenses.isEmpty()) {
597             this.readLicenses();
598         }
599         if (approvedFamilies.isEmpty()) {
600             SortedSet<String> result = new TreeSet<>();
601             licenses.stream().map(x -> x.getLicenseFamily().getFamilyCategory()).forEach(result::add);
602             return result;
603         }
604         return Collections.unmodifiableSortedSet(approvedFamilies);
605     }
606 
607     private void parseMatcherBuilder(final Node classNode) {
608         try {
609             Map<String, String> attributes = attributes(classNode);
610             if (attributes.get(XMLConfig.ATT_CLASS_NAME) == null) {
611                 throw new ConfigurationException("matcher must have a " + XMLConfig.ATT_CLASS_NAME + " attribute");
612             }
613             MatcherBuilderTracker.addBuilder(attributes.get(XMLConfig.ATT_CLASS_NAME), attributes.get(XMLConfig.ATT_NAME));
614         } catch (RuntimeException exception) {
615             DefaultLog.getInstance().error(String.format("Matcher error in: '%s'", nodeText(classNode)));
616             throw exception;
617         }
618     }
619 
620     @Override
621     public void readMatcherBuilders() {
622         nodeListConsumer(document.getElementsByTagName(XMLConfig.MATCHER), this::parseMatcherBuilder);
623     }
624 
625     @Override
626     public void addMatchers(final URI uri) {
627         read(uri);
628     }
629 
630     /**
631      * An abstract builder that delegates to another abstract builder.
632      */
633     static class IDRecordingBuilder extends AbstractBuilder {
634         /** The builder we are delegating to */
635         private final AbstractBuilder delegate;
636         /**
637          * The map of matchers that the system is building during processing.
638          * We will utilize this to set the matcher value later.
639          */
640         private final Map<String, IHeaderMatcher> matchers;
641 
642         IDRecordingBuilder(final Map<String, IHeaderMatcher> matchers, final AbstractBuilder delegate) {
643             this.delegate = delegate;
644             this.matchers = matchers;
645             setId(delegate.getId());
646         }
647 
648         @Override
649         public IHeaderMatcher build() {
650             IHeaderMatcher result = delegate.build();
651             matchers.put(result.getId(), result);
652             return result;
653         }
654 
655         @Override
656         public Description getDescription() {
657             return delegate.getDescription();
658         }
659     }
660 }