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.HashSet;
24  import java.util.List;
25  import java.util.Objects;
26  import java.util.Set;
27  import java.util.TreeSet;
28  import java.util.stream.Collectors;
29  
30  import org.apache.rat.document.DocumentName;
31  import org.apache.rat.document.DocumentNameMatcher;
32  import org.apache.rat.utils.DefaultLog;
33  import org.apache.rat.utils.ExtendedIterator;
34  
35  import static java.lang.String.format;
36  
37  /**
38   * Processes the include and exclude patterns and applies the result against a base directory
39   * to return an IReportable that contains all the reportable objects.
40   */
41  public class ExclusionProcessor {
42      /** Strings that identify the files/directories to exclude */
43      private final Set<String> excludedPatterns;
44      /** Path matchers that exclude files/directories */
45      private final List<DocumentNameMatcher> excludedPaths;
46      /** Strings that identify the files/directories to include (overrides exclude) */
47      private final Set<String> includedPatterns;
48      /** Path matchers that identify the files/directories to include (overrides exclude) */
49      private final List<DocumentNameMatcher> includedPaths;
50      /**
51       * Collections of StandardCollections that have file processors that should be
52       * used to add additional exclude files to the process
53       */
54      private final Set<StandardCollection> fileProcessors;
55      /** Standard collections that contribute to the inclusion processing */
56      private final Set<StandardCollection> includedCollections;
57      /** Standard collections that contribute to the exclusion procession */
58      private final Set<StandardCollection> excludedCollections;
59      /** The last generated PathMatcher */
60      private DocumentNameMatcher lastMatcher;
61      /** The base dir for the last PathMatcher */
62      private DocumentName lastMatcherBaseDir;
63  
64      /**
65       * Constructs the processor.
66       */
67      public ExclusionProcessor() {
68          excludedPatterns = new HashSet<>();
69          excludedPaths = new ArrayList<>();
70          includedPatterns = new HashSet<>();
71          includedPaths = new ArrayList<>();
72          fileProcessors = new HashSet<>();
73          includedCollections = new HashSet<>();
74          excludedCollections = new HashSet<>();
75      }
76  
77      /** Reset the {@link #lastMatcher} and {@link #lastMatcherBaseDir} to start again */
78      private void resetLastMatcher() {
79          lastMatcher = null;
80          lastMatcherBaseDir = null;
81      }
82  
83      /**
84       * Add the Iterable of strings to the collection of file/directory patters to ignore.
85       * @param patterns the patterns to add
86       * @return this
87       */
88      public ExclusionProcessor addIncludedPatterns(final Iterable<String> patterns) {
89          DefaultLog.getInstance().debug(format("Including patterns: %s", String.join(", ", patterns)));
90          patterns.forEach(includedPatterns::add);
91          resetLastMatcher();
92          return this;
93      }
94  
95      /**
96       * Add a DocumentNameMatcher to the collection of file/directory patterns to ignore.
97       * @param matcher the DocumentNameMatcher to add. Will be ignored if {@code null}.
98       * @return this
99       */
100     public ExclusionProcessor addIncludedMatcher(final DocumentNameMatcher matcher) {
101         if (matcher != null) {
102             includedPaths.add(matcher);
103             resetLastMatcher();
104         }
105         return this;
106     }
107 
108     /**
109      * Add the file processor from a StandardCollection.
110      * @param collection the collection to add the processor from.
111      * @return this
112      */
113     public ExclusionProcessor addFileProcessor(final StandardCollection collection) {
114         if (collection != null) {
115             DefaultLog.getInstance().debug(format("Processing exclude file from %s.", collection));
116             fileProcessors.add(collection);
117             resetLastMatcher();
118         }
119         return this;
120     }
121 
122     /**
123      * Add the patterns from the StandardCollection as included patterns.
124      * @param collection the standard collection to add the includes from.
125      * @return this
126      */
127     public ExclusionProcessor addIncludedCollection(final StandardCollection collection) {
128         if (collection != null) {
129             DefaultLog.getInstance().debug(format("Including %s collection.", collection));
130             includedCollections.add(collection);
131             resetLastMatcher();
132         }
133         return this;
134     }
135 
136     /**
137      * Add the patterns from collections of patterns as excluded patterns.
138      * @param patterns the strings to that define patterns to be excluded from processing.
139      * @return this
140      */
141     public ExclusionProcessor addExcludedPatterns(final Iterable<String> patterns) {
142         DefaultLog.getInstance().debug(format("Excluding patterns: %s", String.join(", ", patterns)));
143         patterns.forEach(excludedPatterns::add);
144         resetLastMatcher();
145         return this;
146     }
147 
148     /**
149      * Add the DocumentNameMatcher as an excluded pattern.
150      * @param matcher the DocumentNameMatcher to exclude.
151      * @return this
152      */
153     public ExclusionProcessor addExcludedMatcher(final DocumentNameMatcher matcher) {
154         if (matcher != null) {
155             excludedPaths.add(matcher);
156             resetLastMatcher();
157         }
158         return this;
159     }
160 
161     /**
162      * Report the excluded files to the appendable object.
163      * @param appendable the appendable object to write to.
164      */
165     public void reportExclusions(final Appendable appendable) throws IOException {
166         appendable.append(format("Excluding patterns: %s%n", String.join(", ", excludedPatterns)));
167         appendable.append(format("Including patterns: %s%n", String.join(", ", includedPatterns)));
168         for (StandardCollection sc : excludedCollections) {
169             appendable.append(format("Excluding %s collection.%n", sc.name()));
170         }
171         for (StandardCollection sc : includedCollections) {
172             appendable.append(format("Including %s collection.%n", sc.name()));
173         }
174         for (StandardCollection sc : fileProcessors) {
175             appendable.append(format("Processing exclude file from %s.%n", sc.name()));
176         }
177     }
178 
179 
180     /**
181      * Excludes the files/directories specified by a StandardCollection.
182      * @param collection the StandardCollection that identifies the files to exclude.
183      * @return this
184      */
185     public ExclusionProcessor addExcludedCollection(final StandardCollection collection) {
186         if (collection != null) {
187             DefaultLog.getInstance().debug(format("Excluding %s collection.", collection));
188             excludedCollections.add(collection);
189             resetLastMatcher();
190         }
191         return this;
192     }
193 
194     /**
195      * Creates a Document name matcher that will return {@code false} on any
196      * document that is excluded.
197      * @param basedir the base directory to make everything relative to.
198      * @return A DocumentNameMatcher that will return {@code false} for any document that is to be excluded.
199      */
200     public DocumentNameMatcher getNameMatcher(final DocumentName basedir) {
201         // if lastMatcher is not set or the basedir is not the same as the last one then
202         // we have to regenerate the matching document. Otherwise we can just return the
203         // lastMatcher since there is no change.
204         if (lastMatcher == null || !basedir.equals(lastMatcherBaseDir)) {
205             lastMatcherBaseDir = basedir;
206 
207             // add the file processors
208             final List<MatcherSet> matchers = extractFileProcessors(basedir);
209             final MatcherSet.Builder fromCommandLine = new MatcherSet.Builder();
210             DocumentName.Builder nameBuilder = DocumentName.builder(basedir).setBaseName(basedir);
211             extractPatterns(nameBuilder, fromCommandLine);
212             extractCollectionPatterns(nameBuilder, fromCommandLine);
213             extractCollectionMatchers(fromCommandLine);
214             extractPaths(fromCommandLine);
215             matchers.add(fromCommandLine.build());
216 
217             lastMatcher = MatcherSet.merge(matchers).createMatcher();
218             DefaultLog.getInstance().debug(format("Created matcher set for %s%n%s", basedir.getName(),
219                     lastMatcher));
220         }
221         return lastMatcher;
222     }
223 
224     /**
225      * Extracts the file processors from {@link #fileProcessors}.
226      * @param basedir The directory to base the file processors on.
227      * @return a list of MatcherSets that are created for each {@link #fileProcessors} entry.
228      */
229     private List<MatcherSet> extractFileProcessors(final DocumentName basedir) {
230         final List<MatcherSet> fileProcessorList = new ArrayList<>();
231         for (StandardCollection sc : fileProcessors) {
232             ExtendedIterator<List<MatcherSet>> iter =  sc.fileProcessorBuilder().map(builder -> builder.build(basedir));
233             if (iter.hasNext()) {
234                 iter.forEachRemaining(fileProcessorList::addAll);
235             } else {
236                 DefaultLog.getInstance().debug(String.format("%s does not have a fileProcessor.", sc));
237             }
238         }
239         return fileProcessorList;
240     }
241 
242     /**
243      * Converts the pattern to use the directory separator specified by the document name and localises it for
244      * exclusion processing.
245      * @param documentName The document name to adjust the pattern against.
246      * @param pattern the pattern.
247      * @return the prepared pattern.
248      */
249     private String preparePattern(final DocumentName documentName, final String pattern) {
250         return ExclusionUtils.qualifyPattern(documentName,
251                         ExclusionUtils.convertSeparator(pattern, "/", documentName.getDirectorySeparator()));
252     }
253 
254     /**
255      * Extracts {@link #includedPatterns} and {@link #excludedPatterns} into the specified matcherBuilder.
256      * @param nameBuilder The name builder for the pattern. File names are resolved against the generated name.
257      * @param matcherBuilder the MatcherSet.Builder to add the patterns to.
258      */
259     private void extractPatterns(final DocumentName.Builder nameBuilder, final MatcherSet.Builder matcherBuilder) {
260         DocumentName name = nameBuilder.setName("Patterns").build();
261         if (!excludedPatterns.isEmpty()) {
262             matcherBuilder.addExcluded(name, excludedPatterns.stream()
263                     .map(s -> preparePattern(name, s))
264                     .collect(Collectors.toSet()));
265         }
266         if (!includedPatterns.isEmpty()) {
267             matcherBuilder.addIncluded(name, includedPatterns.stream()
268                     .map(s -> preparePattern(name, s)).collect(Collectors.toSet()));
269         }
270     }
271 
272     /**
273      * Extracts {@link #includedCollections} and {@link #excludedCollections} patterns into the specified matcherBuilder.
274      * @param nameBuilder the name builder for the pattern names.
275      * @param matcherBuilder the MatcherSet.Builder to add the collections to.
276      */
277     private void extractCollectionPatterns(final DocumentName.Builder nameBuilder, final MatcherSet.Builder matcherBuilder) {
278         final Set<String> incl = new TreeSet<>();
279         final Set<String> excl = new TreeSet<>();
280         for (StandardCollection sc : includedCollections) {
281             Set<String> patterns = sc.patterns();
282             if (patterns.isEmpty()) {
283                 DefaultLog.getInstance().debug(String.format("%s does not have a defined collection for inclusion.", sc));
284             } else {
285                 MatcherSet.Builder.segregateList(incl, excl, sc.patterns());
286             }
287         }
288         for (StandardCollection sc : excludedCollections) {
289             Set<String> patterns = sc.patterns();
290             if (patterns.isEmpty()) {
291                 DefaultLog.getInstance().debug(String.format("%s does not have a defined collection for exclusion.", sc));
292             } else {
293                 MatcherSet.Builder.segregateList(excl, incl, sc.patterns());
294             }
295         }
296         DocumentName name = nameBuilder.setName("Collections").build();
297         matcherBuilder
298                 .addExcluded(name, excl.stream().map(s -> preparePattern(name.getBaseDocumentName(), s)).collect(Collectors.toSet()))
299                 .addIncluded(name, incl.stream().map(s -> preparePattern(name.getBaseDocumentName(), s)).collect(Collectors.toSet()));
300     }
301 
302     /**
303      * Extracts {@link #includedCollections} and {@link #excludedCollections} matchers into the specified matcherBuilder.
304      * @param matcherBuilder the MatcherSet.Builder to add the collections to.
305      */
306     private void extractCollectionMatchers(final MatcherSet.Builder matcherBuilder) {
307         ExtendedIterator.create(includedCollections.iterator())
308                 .map(StandardCollection::staticDocumentNameMatcher)
309                 .filter(Objects::nonNull)
310                 .forEachRemaining(matcherBuilder::addIncluded);
311 
312         ExtendedIterator.create(excludedCollections.iterator())
313                 .map(StandardCollection::staticDocumentNameMatcher)
314                 .filter(Objects::nonNull)
315                 .forEachRemaining(matcherBuilder::addExcluded);
316     }
317 
318     /**
319      * Extracts {@link #includedPaths} and {@link #excludedPaths} patterns into the specified matcherBuilder.
320      * @param matcherBuilder the MatcherSet.Builder to add the collections to.
321      */
322     private void extractPaths(final MatcherSet.Builder matcherBuilder) {
323         if (!includedPaths.isEmpty()) {
324             for (DocumentNameMatcher matcher : includedPaths) {
325                 DefaultLog.getInstance().debug(format("Including path matcher %s", matcher));
326                 matcherBuilder.addIncluded(matcher);
327             }
328         }
329         if (!excludedPaths.isEmpty()) {
330             for (DocumentNameMatcher matcher : excludedPaths) {
331                 DefaultLog.getInstance().debug(format("Excluding path matcher %s", matcher));
332                 matcherBuilder.addExcluded(matcher);
333             }
334         }
335     }
336 
337 }