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