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  
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   * A class that reads the XML configuration file format.
66   */
67  public final class XMLConfigurationReader implements LicenseReader, MatcherReader {
68      /** The document we are building */
69      private Document document;
70      /** The root element in the document */
71      private final Element rootElement;
72      /** The families element in the document */
73      private final Element familiesElement;
74      /** The licenses element in the document */
75      private final Element licensesElement;
76      /** The approved element in the document */
77      private final Element approvedElement;
78      /** The matchers element in the document */
79      private final Element matchersElement;
80      /** The sorted set of licenses */
81      private final SortedSet<ILicense> licenses;
82      /** The map of matcher ids to matcher */
83      private final Map<String, IHeaderMatcher> matchers;
84      /** The builder parameters */
85      private final BuilderParams builderParams;
86      /** The sorted set of license families */
87      private final SortedSet<ILicenseFamily> licenseFamilies;
88      /** The sorted set of approved license family categories */
89      private final SortedSet<String> approvedFamilies;
90  
91      /**
92       * Constructs the XML configuration reader.
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      * Creates textual representation of a node for display.
125      * @param node the node to create the textual representation of.
126      * @return the textual representation of the node for display.
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      * Read xml from a reader.
145      * @param reader the reader to read XML from.
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      * Read the uris and extract the DOM information to create new objects.
157      * @param uris the URIs to read.
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      * Applies the {@code consumer} to each node in the {@code list}. Generally used for extracting info from a
172      * {@code NodeList}.
173      * @param list the NodeList to process.
174      * @param consumer the consumer to apply to each node in the list.
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      * Merge the new document into the document that this reader processes is building.
184      * @param newDoc the Document to merge.
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      * Get a map of Node attribute names to values.
199      * @param node the node to process.
200      * @return the map of attributes on the node.
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      * Finds the setter description property in the builder and set it with the value.
214      * @param desc the description for the setter.
215      * @param builder the builder to set the value in.
216      * @param value the value to set.
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      * For any children of description that are BUILD_PARAMETERS set the builder property.
229      * @param description the description for the builder.
230      * @param builder the builder to set the properties in.
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      * Processes a list of children by passing each child node and the description
245      * of the child (if any) to the BiPredicate. If there is not a child description
246      * for the node it is ignored. If the node is processed it is removed from list
247      * of children.
248      * @param description the description of the node being processed.
249      * @param children the child nodes of that node.
250      * @param childProcessor the function that handles the processing of the child
251      * node.
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      * Creates a child node processor for the builder described by the description.
269      * @param builder the builder to set properties in.
270      * @param description the description of the builder.
271      * @return child node.
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      * Sets the value of the element described by the description in the builder with the value from childDescription.
300      * @param description the property in the builder to set.
301      * @param childDescription the description of the child property to extract.
302      * @param builder the builder to set the value in.
303      * @param child the child to extract the value from.
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      * Process the ELEMENT_NODEs children of the parent whose names match child
316      * descriptions. All children of children are processed with the childProcessor. If
317      * the childProcessor handles the node it is not included in the resulting list.
318      * @param description the description of the parent node.
319      * @param parent the node being processed.
320      * @param childProcessor the BiProcessor to handle process each child. If the
321      * processor handles the child it must return {@code true}.
322      * @return a Pair comprising a boolean flag indicating children were found, and
323      * a list of all child nodes that were not processed by the childProcessor.
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             // check XML child nodes.
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      * Creates a Builder from a Matcher node.
352      * @param matcherNode the Matcher node to parse.
353      * @return the Builder for the matcher described by the node.
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             // process the attributes
366             description.setChildren(builder, attributes(matcherNode));
367 
368             // check XML child nodes.
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             // check for inline nodes that can accept child nodes.
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                     // can only process text inline if there were no child nodes.
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      * Parses a license from a license node.
445      * @param licenseNode the node to parse.
446      * @return the License definition.
447      */
448     private ILicense parseLicense(final Node licenseNode) {
449         try {
450             ILicense.Builder builder = ILicense.builder();
451             // get the description for the builder
452             Description description = builder.getDescription();
453             // set the BUILDER_PARAM options from the description
454             processBuilderParams(description, builder);
455             // set the children from attributes.
456             description.setChildren(builder, attributes(licenseNode));
457             // set children from the child nodes
458             Pair<Boolean, List<Node>> pair = processChildNodes(description, licenseNode,
459                     licenseChildNodeProcessor(builder, description));
460             List<Node> children = pair.getRight();
461 
462             // check for inline nodes that can accept child nodes.
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      * Parses a license family from a map that contains the ID and Name attributes.
510      * @param attributes the map of attributes.
511      * @return the license family defined in the map.
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      * Parses a license family node into a license family and adds it to the license families set.
526      * @param familyNode the node to parse.
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      * Parse an approved License family and adds it to the set of license families as well as the
546      * set of approved license families.
547      * @param approvedNode the node to parse.
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     ////////////////////////////////////////// MatcherReader methods
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      * An abstract builder that delegates to another abstract builder.
614      */
615     static class IDRecordingBuilder extends AbstractBuilder {
616         /** The builder we are delegating to */
617         private final AbstractBuilder delegate;
618         /**
619          * The map of matchers that the system is building during processing.
620          * We will utilize this to set the matcher value later.
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 }