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