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.config.exclusion;
20  
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Set;
29  import java.util.TreeSet;
30  import java.util.function.Predicate;
31  import java.util.stream.Collectors;
32  
33  import org.apache.commons.lang3.NotImplementedException;
34  import org.apache.rat.configuration.XMLConfigurationReader;
35  import org.apache.rat.document.DocumentName;
36  import org.apache.rat.document.DocumentNameMatcher;
37  import org.apache.rat.report.xml.writer.XmlWriter;
38  import org.apache.rat.utils.DefaultLog;
39  import org.apache.rat.utils.ExtendedIterator;
40  import org.w3c.dom.Node;
41  import org.w3c.dom.NodeList;
42  
43  import static java.lang.String.format;
44  
45  /**
46   * Processes the include and exclude patterns and applies the result against a base directory
47   * to return a Reportable that contains all the reportable objects.
48   */
49  public class ExclusionProcessor {
50      /** Strings that identify the files/directories to exclude */
51      private final Set<String> excludedPatterns;
52      /** Path matchers that exclude files/directories */
53      private final List<DocumentNameMatcher> excludedPaths;
54      /** Strings that identify the files/directories to include (overrides exclude) */
55      private final Set<String> includedPatterns;
56      /** Path matchers that identify the files/directories to include (overrides exclude) */
57      private final List<DocumentNameMatcher> includedPaths;
58      /**
59       * Collections of StandardCollections that have file processors that should be
60       * used to add additional exclude files to the process
61       */
62      private final Set<StandardCollection> fileProcessors;
63      /** Standard collections that contribute to the inclusion processing */
64      private final Set<StandardCollection> includedCollections;
65      /** Standard collections that contribute to the exclusion procession */
66      private final Set<StandardCollection> excludedCollections;
67      /** The last generated PathMatcher */
68      private DocumentNameMatcher lastMatcher;
69      /** The base dir for the last PathMatcher */
70      private DocumentName lastMatcherBaseDir;
71  
72      /**
73       * Constructs the processor.
74       */
75      public ExclusionProcessor() {
76          excludedPatterns = new HashSet<>();
77          excludedPaths = new ArrayList<>();
78          includedPatterns = new HashSet<>();
79          includedPaths = new ArrayList<>();
80          fileProcessors = new HashSet<>();
81          includedCollections = new HashSet<>();
82          excludedCollections = new HashSet<>();
83      }
84  
85      public SerDes serDes() {
86          return new SerDes();
87      }
88  
89      /* the following set of methods are here for testing purposes */
90      Set<String> getExcludedPatterns() {
91          return new HashSet<>(excludedPatterns);
92      }
93  
94      Collection<DocumentNameMatcher> getExcludedPaths() {
95          return new ArrayList<>(excludedPaths);
96      }
97  
98      Set<String> getIncludedPatterns() {
99          return new HashSet<>(includedPatterns);
100     }
101 
102     Collection<DocumentNameMatcher> getIncludedPaths() {
103         return new ArrayList<>(includedPaths);
104     }
105 
106     Collection<StandardCollection> getFileProcessors() {
107         return new HashSet<>(fileProcessors);
108     }
109 
110     Set<StandardCollection> getIncludedCollections() {
111         return new HashSet<>(includedCollections);
112     }
113 
114     Set<StandardCollection> getExcludedCollections() {
115         return new HashSet<>(excludedCollections);
116     }
117 
118     DocumentNameMatcher getLastMatcher() {
119         return lastMatcher;
120     }
121 
122     DocumentName getLastMatcherBaseDir() {
123         return lastMatcherBaseDir;
124     }
125 
126     /**
127      * Reset the {@link #lastMatcher} and {@link #lastMatcherBaseDir} to start again.
128      */
129     private void resetLastMatcher() {
130         lastMatcher = null;
131         lastMatcherBaseDir = null;
132     }
133 
134     /**
135      * Add the iterable of strings to the collection of file/directory patters to ignore.
136      * @param patterns the patterns to add.
137      * @return this
138      */
139     public ExclusionProcessor addIncludedPatterns(final Iterable<String> patterns) {
140         if (patterns != null) {
141         DefaultLog.getInstance().debug(format("Including patterns: %s", String.join(", ", patterns)));
142         patterns.forEach(includedPatterns::add);
143         resetLastMatcher();
144         }
145         return this;
146     }
147 
148     /**
149      * Add a DocumentNameMatcher to the collection of file/directory patterns to ignore.
150      * @param matcher the DocumentNameMatcher to add. Will be ignored if {@code null}.
151      * @return this
152      */
153     public ExclusionProcessor addIncludedMatcher(final DocumentNameMatcher matcher) {
154         if (matcher != null) {
155             includedPaths.add(matcher);
156             resetLastMatcher();
157         }
158         return this;
159     }
160 
161     /**
162      * Add the file processor from a StandardCollection.
163      * @param collection the collection to add the processor from.
164      * @return this
165      */
166     public ExclusionProcessor addFileProcessor(final StandardCollection collection) {
167         if (collection != null) {
168             DefaultLog.getInstance().debug(format("Processing exclude file from %s.", collection));
169             fileProcessors.add(collection);
170             resetLastMatcher();
171         }
172         return this;
173     }
174 
175     /**
176      * Add the patterns from the StandardCollection as included patterns.
177      * @param collection the standard collection to add the includes from.
178      * @return this
179      */
180     public ExclusionProcessor addIncludedCollection(final StandardCollection collection) {
181         if (collection != null) {
182             DefaultLog.getInstance().debug(format("Including %s collection.", collection));
183             includedCollections.add(collection);
184             resetLastMatcher();
185         }
186         return this;
187     }
188 
189     /**
190      * Add the patterns from collections of patterns as excluded patterns.
191      * @param patterns the strings to that define patterns to be excluded from processing.
192      * @return this
193      */
194     public ExclusionProcessor addExcludedPatterns(final Iterable<String> patterns) {
195         if (patterns != null) {
196         DefaultLog.getInstance().debug(format("Excluding patterns: %s", String.join(", ", patterns)));
197         patterns.forEach(excludedPatterns::add);
198         resetLastMatcher();
199         }
200         return this;
201     }
202 
203     /**
204      * Add the DocumentNameMatcher as an excluded pattern.
205      * @param matcher the DocumentNameMatcher to exclude.
206      * @return this
207      */
208     public ExclusionProcessor addExcludedMatcher(final DocumentNameMatcher matcher) {
209         if (matcher != null) {
210             excludedPaths.add(matcher);
211             resetLastMatcher();
212         }
213         return this;
214     }
215 
216     /**
217      * Report the excluded files to the appendable object.
218      * @param appendable the appendable object to write to.
219      */
220     public void reportExclusions(final Appendable appendable) throws IOException {
221         appendable.append(format("Excluding patterns: %s%n", String.join(", ", excludedPatterns)));
222         appendable.append(format("Including patterns: %s%n", String.join(", ", includedPatterns)));
223         for (StandardCollection sc : excludedCollections) {
224             appendable.append(format("Excluding %s collection.%n", sc.name()));
225         }
226         for (StandardCollection sc : includedCollections) {
227             appendable.append(format("Including %s collection.%n", sc.name()));
228         }
229         for (StandardCollection sc : fileProcessors) {
230             appendable.append(format("Processing exclude file from %s.%n", sc.name()));
231         }
232         for (DocumentNameMatcher nameMatcher : excludedPaths) {
233             appendable.append(format("Excluding %s.%n", nameMatcher.toString()));
234         }
235         for (DocumentNameMatcher nameMatcher : includedPaths) {
236             appendable.append(format("Including %s.%n", nameMatcher.toString()));
237         }
238     }
239 
240     /**
241      * Excludes the files/directories specified by a StandardCollection.
242      * @param collection the StandardCollection that identifies the files to exclude.
243      * @return this
244      */
245     public ExclusionProcessor addExcludedCollection(final StandardCollection collection) {
246         if (collection != null) {
247             DefaultLog.getInstance().debug(format("Excluding %s collection.", collection));
248             excludedCollections.add(collection);
249             resetLastMatcher();
250         }
251         return this;
252     }
253 
254     /**
255      * Creates a Document name matcher that will return {@code false} on any
256      * document that is excluded.
257      * @param basedir the base directory to make everything relative to.
258      * @return A DocumentNameMatcher that will return {@code false} for any document that is to be excluded.
259      */
260     public DocumentNameMatcher getNameMatcher(final DocumentName basedir) {
261         // if lastMatcher is not set or the basedir is not the same as the last one then
262         // we have to regenerate the matching document.
263         // Otherwise, we can just return the lastMatcher since there is no change.
264         if (lastMatcher == null || !basedir.equals(lastMatcherBaseDir)) {
265             lastMatcherBaseDir = basedir;
266 
267             // add the file processors
268             final List<MatcherSet> matchers = extractFileProcessors(basedir);
269             final MatcherSet.Builder fromCommandLine = new MatcherSet.Builder();
270             DocumentName.Builder nameBuilder = DocumentName.builder(basedir).setBaseName(basedir);
271             extractPatterns(nameBuilder, fromCommandLine);
272             extractCollectionPatterns(nameBuilder, fromCommandLine);
273             extractCollectionMatchers(fromCommandLine);
274             extractPaths(fromCommandLine);
275             matchers.add(fromCommandLine.build());
276 
277             lastMatcher = MatcherSet.merge(matchers).createMatcher();
278             DefaultLog.getInstance().debug(format("Created matcher set for %s%n%s", basedir.getName(),
279                     lastMatcher));
280         }
281         return lastMatcher;
282     }
283 
284     /**
285      * Extracts the file processors from {@link #fileProcessors}.
286      * @param basedir The directory to base the file processors on.
287      * @return a list of MatcherSets that are created for each {@link #fileProcessors} entry.
288      */
289     private List<MatcherSet> extractFileProcessors(final DocumentName basedir) {
290         final List<MatcherSet> fileProcessorList = new ArrayList<>();
291         for (StandardCollection sc : fileProcessors) {
292             ExtendedIterator<List<MatcherSet>> iter =  sc.fileProcessorBuilder().map(builder -> builder.build(basedir));
293             if (iter.hasNext()) {
294                 iter.forEachRemaining(fileProcessorList::addAll);
295             } else {
296                 DefaultLog.getInstance().debug(String.format("%s does not have a fileProcessor.", sc));
297             }
298         }
299         return fileProcessorList;
300     }
301 
302     /**
303      * Converts the pattern to use the directory separator specified by the document name and localises it for
304      * exclusion processing.
305      * @param documentName The document name to adjust the pattern against.
306      * @param pattern the pattern.
307      * @return the prepared pattern.
308      */
309     private String preparePattern(final DocumentName documentName, final String pattern) {
310         return ExclusionUtils.qualifyPattern(documentName,
311                         ExclusionUtils.convertSeparator(pattern, "/", documentName.getDirectorySeparator()));
312     }
313 
314     /**
315      * Extracts {@link #includedPatterns} and {@link #excludedPatterns} into the specified matcherBuilder.
316      * @param nameBuilder The name builder for the pattern. File names are resolved against the generated name.
317      * @param matcherBuilder the MatcherSet.Builder to add the patterns to.
318      */
319     private void extractPatterns(final DocumentName.Builder nameBuilder, final MatcherSet.Builder matcherBuilder) {
320         DocumentName name = nameBuilder.setName("Patterns").build();
321         if (!excludedPatterns.isEmpty()) {
322             matcherBuilder.addExcluded(name, excludedPatterns.stream()
323                     .map(s -> preparePattern(name, s))
324                     .collect(Collectors.toSet()));
325         }
326         if (!includedPatterns.isEmpty()) {
327             matcherBuilder.addIncluded(name, includedPatterns.stream()
328                     .map(s -> preparePattern(name, s)).collect(Collectors.toSet()));
329         }
330     }
331 
332     /**
333      * Extracts {@link #includedCollections} and {@link #excludedCollections} patterns into the specified matcherBuilder.
334      * @param nameBuilder the name builder for the pattern names.
335      * @param matcherBuilder the MatcherSet.Builder to add the collections to.
336      */
337     private void extractCollectionPatterns(final DocumentName.Builder nameBuilder, final MatcherSet.Builder matcherBuilder) {
338         final Set<String> incl = new TreeSet<>();
339         final Set<String> excl = new TreeSet<>();
340         for (StandardCollection sc : includedCollections) {
341             Set<String> patterns = sc.patterns();
342             if (patterns.isEmpty()) {
343                 DefaultLog.getInstance().debug(String.format("%s does not have a defined collection for inclusion.", sc));
344             } else {
345                 MatcherSet.Builder.segregateList(incl, excl, sc.patterns());
346             }
347         }
348         for (StandardCollection sc : excludedCollections) {
349             Set<String> patterns = sc.patterns();
350             if (patterns.isEmpty()) {
351                 DefaultLog.getInstance().debug(String.format("%s does not have a defined collection for exclusion.", sc));
352             } else {
353                 MatcherSet.Builder.segregateList(excl, incl, sc.patterns());
354             }
355         }
356         DocumentName name = nameBuilder.setName("Collections").build();
357         matcherBuilder
358                 .addExcluded(name, excl.stream().map(s -> preparePattern(name.getBaseDocumentName(), s)).collect(Collectors.toSet()))
359                 .addIncluded(name, incl.stream().map(s -> preparePattern(name.getBaseDocumentName(), s)).collect(Collectors.toSet()));
360     }
361 
362     /**
363      * Extracts {@link #includedCollections} and {@link #excludedCollections} matchers into the specified matcherBuilder.
364      * @param matcherBuilder the MatcherSet.Builder to add the collections to.
365      */
366     private void extractCollectionMatchers(final MatcherSet.Builder matcherBuilder) {
367         ExtendedIterator.create(includedCollections.iterator())
368                 .map(StandardCollection::staticDocumentNameMatcher)
369                 .filter(Objects::nonNull)
370                 .forEachRemaining(matcherBuilder::addIncluded);
371 
372         ExtendedIterator.create(excludedCollections.iterator())
373                 .map(StandardCollection::staticDocumentNameMatcher)
374                 .filter(Objects::nonNull)
375                 .forEachRemaining(matcherBuilder::addExcluded);
376     }
377 
378     /**
379      * Extracts {@link #includedPaths} and {@link #excludedPaths} patterns into the specified matcherBuilder.
380      * @param matcherBuilder the MatcherSet.Builder to add the collections to.
381      */
382     private void extractPaths(final MatcherSet.Builder matcherBuilder) {
383         if (!includedPaths.isEmpty()) {
384             for (DocumentNameMatcher matcher : includedPaths) {
385                 DefaultLog.getInstance().debug(format("Including path matcher %s", matcher));
386                 matcherBuilder.addIncluded(matcher);
387             }
388         }
389         if (!excludedPaths.isEmpty()) {
390             for (DocumentNameMatcher matcher : excludedPaths) {
391                 DefaultLog.getInstance().debug(format("Excluding path matcher %s", matcher));
392                 matcherBuilder.addExcluded(matcher);
393             }
394         }
395     }
396 
397     /**
398      * Serializes and deserializes the ExclusionProcessor to an XML document.
399      */
400     public class SerDes {
401         /** The pattern attribute name */
402         private static final String PATTERN = "pattern";
403         /** THe name attribute name */
404         private static final String NAME = "name";
405 
406         /**
407          * Serialize the ExclusionProcessor to XML writer.
408          * @param writer the writer to serialize to.
409          * @throws IOException on Error
410          */
411         public void serialize(final XmlWriter writer) throws IOException {
412             writer.startElement("ExclusionProcessor");
413 
414             for (String pattern : excludedPatterns) {
415                 writer.startElement("excludedPattern").attribute(PATTERN, pattern).closeElement();
416             }
417             for (StandardCollection obj : excludedCollections) {
418                 writer.startElement("excludedCollection").attribute(NAME, obj.name()).closeElement();
419             }
420             for (DocumentNameMatcher obj : excludedPaths) {
421                 writer.startElement("excludedPath").attribute(NAME, obj.toString()).closeElement();
422             }
423 
424             for (String pattern : includedPatterns) {
425                 writer.startElement("includedPattern").attribute(PATTERN, pattern).closeElement();
426             }
427             for (StandardCollection obj : includedCollections) {
428                 writer.startElement("includedCollection").attribute(NAME, obj.name()).closeElement();
429             }
430             for (DocumentNameMatcher obj : includedPaths) {
431                 writer.startElement("includedPath").attribute(NAME, obj.toString()).closeElement();
432             }
433 
434             for (StandardCollection obj : fileProcessors) {
435                 writer.startElement("fileProcessor").attribute(NAME, obj.name()).closeElement();
436             }
437             writer.closeElement();
438         }
439 
440         /**
441          * Deserialize from XML Document node to ExclusionProcessor.
442          * @param xmlNode the node to deserialize from.
443          */
444         public void deserialize(final Node xmlNode) {
445             final NodeList children = xmlNode.getChildNodes();
446             for (int i = 0; i < children.getLength(); i++) {
447                 Node child = children.item(i);
448                 Map<String, String> attributes = XMLConfigurationReader.attributes(child);
449                 switch (child.getNodeName()) {
450                     case "excludedPattern" ->
451                         excludedPatterns.add(attributes.get(PATTERN));
452 
453                     case "excludedCollection" ->
454                         excludedCollections.add(StandardCollection.valueOf(attributes.get(NAME)));
455 
456                     case "excludedPath" ->
457                         excludedPaths.add(new DocumentNameMatcher(attributes.get(NAME),
458                                 (Predicate<DocumentName>) x -> {
459                                     throw new NotImplementedException("Deserialized ExclusionProcessor can not evaluate excluded paths");
460                                 }));
461 
462                     case "includedPattern" ->
463                         includedPatterns.add(attributes.get(PATTERN));
464 
465                     case "includedCollection" ->
466                         includedCollections.add(StandardCollection.valueOf(attributes.get(NAME)));
467 
468                     case "includedPath" ->
469                         includedPaths.add(new DocumentNameMatcher(attributes.get(NAME),
470                                 (Predicate<DocumentName>) x -> {
471                                     throw new NotImplementedException("Deserialized ExclusionProcessor can not evaluate included paths");
472                                 }));
473 
474                     case "fileProcessor" ->
475                         fileProcessors.add(StandardCollection.valueOf(attributes.get(NAME)));
476 
477                     default ->
478                         DefaultLog.getInstance().error(String.format("Unknown child node '%s'", child.getNodeName()));
479                 }
480             }
481         }
482     }
483 }