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.document;
20  
21  import java.io.File;
22  import java.io.FileFilter;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Optional;
29  import java.util.Set;
30  import java.util.function.Predicate;
31  
32  import org.apache.rat.ConfigurationException;
33  import org.apache.rat.config.exclusion.plexus.MatchPattern;
34  import org.apache.rat.config.exclusion.plexus.MatchPatterns;
35  
36  import static java.lang.String.format;
37  
38  /**
39   * Matches document names.
40   */
41  public final class DocumentNameMatcher {
42  
43      /** The predicate that does the actual matching. */
44      private final Predicate<DocumentName> predicate;
45      /** The name of this matcher. */
46      private final String name;
47      /** {@code true} if this matcher is a collection of matchers. */
48      private final boolean isCollection;
49  
50      /**
51       * A matcher that matches all documents.
52       */
53      public static final DocumentNameMatcher MATCHES_ALL = new DocumentNameMatcher("TRUE", (Predicate<DocumentName>) x -> true);
54  
55      /**
56       * A matcher that matches no documents.
57       */
58      public static final DocumentNameMatcher MATCHES_NONE = new DocumentNameMatcher("FALSE", (Predicate<DocumentName>) x -> false);
59  
60      /**
61       * Constructs a DocumentNameMatcher from a name and a DocumentName predicate.
62       * @param name the name for this matcher.
63       * @param predicate the predicate to determine matches.
64       */
65      public DocumentNameMatcher(final String name, final Predicate<DocumentName> predicate) {
66          this.name = name;
67          this.predicate = predicate;
68          this.isCollection = predicate instanceof CollectionPredicate;
69      }
70  
71      /**
72       * Constructs a DocumentNameMatcher from a name and a delegate DocumentNameMatcher.
73       * @param name the name for this matcher.
74       * @param delegate the delegate to defer to.
75       */
76      public DocumentNameMatcher(final String name, final DocumentNameMatcher delegate) {
77          this(name, delegate::matches);
78      }
79  
80      /**
81       * Constructs a DocumentNameMatcher from a name a MatchPatterns instance and the DocumentName for the base name.
82       * @param name the name of this matcher.
83       * @param patterns the patterns in the matcher.
84       * @param basedir the base directory for the scanning.
85       */
86      public DocumentNameMatcher(final String name, final MatchPatterns patterns, final DocumentName basedir) {
87          this(name, new MatchPatternsPredicate(basedir, patterns));
88      }
89  
90      /**
91       * Tokenizes name for faster Matcher processing.
92       * @param name the name to tokenize.
93       * @param dirSeparator the directory separator.
94       * @return the tokenized name.
95       */
96      private static char[][] tokenize(final String name, final String dirSeparator) {
97          String[] tokenizedName = MatchPattern.tokenizePathToString(name, dirSeparator);
98          char[][] tokenizedNameChar = new char[tokenizedName.length][];
99          for (int i = 0; i < tokenizedName.length; i++) {
100             tokenizedNameChar[i] = tokenizedName[i].toCharArray();
101         }
102         return tokenizedNameChar;
103     }
104 
105     /**
106      * Constructs a DocumentNameMatcher from a name and a DocumentName predicate.
107      * @param name the name of the matcher.
108      * @param matchers fully specified matchers.
109      */
110     public DocumentNameMatcher(final String name, final MatchPatterns matchers) {
111         this(name, new CollectionPredicate() {
112             @Override
113             public Iterable<DocumentNameMatcher> getMatchers() {
114                 final List<DocumentNameMatcher> result = new ArrayList<>();
115                 matchers.patterns().forEach(p -> result.add(new DocumentNameMatcher(p.source(),
116                         (Predicate<DocumentName>) x -> MatchPatterns.from("/", p.source()).matches(x.getName(), x.isCaseSensitive()))));
117                 return result;
118             }
119 
120             @Override
121             public boolean test(final DocumentName documentName) {
122                 return matchers.matches(documentName.getName(), documentName.isCaseSensitive());
123             }
124         });
125     }
126 
127     /**
128      * Creates a DocumentNameMatcher from a File filter.
129      * @param name The name of this matcher.
130      * @param fileFilter the file filter to execute.
131      */
132     public DocumentNameMatcher(final String name, final FileFilter fileFilter) {
133         this(name, new FileFilterPredicate(fileFilter));
134     }
135 
136     /**
137      * Creates a DocumentNameMatcher from a File filter.
138      * @param fileFilter the file filter to execute.
139      */
140     public DocumentNameMatcher(final FileFilter fileFilter) {
141         this(fileFilter.toString(), fileFilter);
142     }
143 
144     public boolean isCollection() {
145         return isCollection;
146     }
147 
148     /**
149      * Returns the predicate that this DocumentNameMatcher is using.
150      * @return The predicate that this DocumentNameMatcher is using.
151      */
152     public Predicate<DocumentName> getPredicate() {
153         return predicate;
154     }
155 
156     @Override
157     public String toString() {
158         return name;
159     }
160 
161     /**
162      * Decomposes the matcher execution against the candidate.
163      * @param candidate the candidate to check.
164      * @return a list of {@link DecomposeData} for each evaluation in the matcher.
165      */
166     public List<DecomposeData> decompose(final DocumentName candidate) {
167         final List<DecomposeData> result = new ArrayList<>();
168         decompose(0, this, candidate, result);
169         return result;
170     }
171 
172     private void decompose(final int level, final DocumentNameMatcher matcher, final DocumentName candidate, final List<DecomposeData> result) {
173         final Predicate<DocumentName> pred = matcher.getPredicate();
174         result.add(new DecomposeData(level, matcher, candidate, pred.test(candidate)));
175     }
176 
177     /**
178      * Performs the match against the DocumentName.
179      * @param documentName the document name to check.
180      * @return true if the documentName matchers this DocumentNameMatcher.
181      */
182     public boolean matches(final DocumentName documentName) {
183         return predicate.test(documentName);
184     }
185 
186     /**
187      * Performs a logical {@code NOT} on a DocumentNameMatcher.
188      * @param nameMatcher the matcher to negate.
189      * @return a PathMatcher that is the negation of the argument.
190      */
191     public static DocumentNameMatcher not(final DocumentNameMatcher nameMatcher) {
192         if (nameMatcher == MATCHES_ALL) {
193             return MATCHES_NONE;
194         }
195         if (nameMatcher == MATCHES_NONE) {
196             return MATCHES_ALL;
197         }
198 
199         return new DocumentNameMatcher(format("not(%s)", nameMatcher), new NotPredicate(nameMatcher));
200     }
201 
202     /**
203      * Joins a collection of DocumentNameMatchers together to create a list of the names.
204      * @param matchers the matchers to extract the names from.
205      * @return the String of the concatenation of the names.
206      */
207     private static String join(final Collection<DocumentNameMatcher> matchers) {
208         List<String> children = new ArrayList<>();
209         matchers.forEach(s -> children.add(s.toString()));
210         return String.join(", ", children);
211     }
212 
213     private static Optional<DocumentNameMatcher> standardCollectionCheck(final Collection<DocumentNameMatcher> matchers, final DocumentNameMatcher override) {
214         if (matchers.isEmpty()) {
215             throw new ConfigurationException("Empty matcher collection");
216         }
217         if (matchers.size() == 1) {
218             return Optional.of(matchers.iterator().next());
219         }
220         if (matchers.contains(override)) {
221             return Optional.of(override);
222         }
223         return Optional.empty();
224     }
225 
226     /**
227      * Performs a logical {@code OR} across the collection of matchers.
228      * @param matchers the matchers to check.
229      * @return a matcher that returns {@code true} if any of the enclosed matchers returns {@code true}.
230      */
231     public static DocumentNameMatcher or(final Collection<DocumentNameMatcher> matchers) {
232         Optional<DocumentNameMatcher> opt = standardCollectionCheck(matchers, MATCHES_ALL);
233         if (opt.isPresent()) {
234             return opt.get();
235         }
236 
237         // preserve order
238         Set<DocumentNameMatcher> workingSet = new LinkedHashSet<>();
239         for (DocumentNameMatcher matcher : matchers) {
240             // check for nested or
241             if (matcher.predicate instanceof Or) {
242                 ((Or) matcher.predicate).getMatchers().forEach(workingSet::add);
243             } else {
244                 workingSet.add(matcher);
245             }
246         }
247         return standardCollectionCheck(matchers, MATCHES_ALL)
248                 .orElseGet(() -> new DocumentNameMatcher(format("or(%s)", join(workingSet)), new Or(workingSet)));
249     }
250 
251     /**
252      * Performs a logical {@code OR} across the collection of matchers.
253      * @param matchers the matchers to check.
254      * @return a matcher that returns {@code true} if any of the enclosed matchers returns {@code true}.
255      */
256     public static DocumentNameMatcher or(final DocumentNameMatcher... matchers) {
257         return or(Arrays.asList(matchers));
258     }
259 
260     /**
261      * Performs a logical {@code AND} across the collection of matchers.
262      * @param matchers the matchers to check.
263      * @return a matcher that returns {@code true} if all the enclosed matchers return {@code true}.
264      */
265     public static DocumentNameMatcher and(final Collection<DocumentNameMatcher> matchers) {
266         Optional<DocumentNameMatcher> opt = standardCollectionCheck(matchers, MATCHES_NONE);
267         if (opt.isPresent()) {
268             return opt.get();
269         }
270 
271         // preserve order
272         Set<DocumentNameMatcher> workingSet = new LinkedHashSet<>();
273         for (DocumentNameMatcher matcher : matchers) {
274             //  check for nexted And
275             if (matcher.predicate instanceof And) {
276                 ((And) matcher.predicate).getMatchers().forEach(workingSet::add);
277             } else {
278                 workingSet.add(matcher);
279             }
280         }
281         opt = standardCollectionCheck(matchers, MATCHES_NONE);
282         return opt.orElseGet(() -> new DocumentNameMatcher(format("and(%s)", join(workingSet)), new And(workingSet)));
283     }
284 
285     /**
286      * A particular matcher that will not match any excluded unless they are listed in the includes.
287      * @param includes the DocumentNameMatcher to match the includes.
288      * @param excludes the DocumentNameMatcher to match the excludes.
289      * @return a DocumentNameMatcher with the specified logic.
290      */
291     public static DocumentNameMatcher matcherSet(final DocumentNameMatcher includes,
292                                                  final DocumentNameMatcher excludes) {
293         if (excludes == MATCHES_NONE) {
294             return MATCHES_ALL;
295         } else {
296             if (includes == MATCHES_NONE) {
297                 return not(excludes);
298             }
299         }
300         if (includes == MATCHES_ALL) {
301             return MATCHES_ALL;
302         }
303         List<DocumentNameMatcher> workingSet = Arrays.asList(includes, excludes);
304         return new DocumentNameMatcher(format("matcherSet(%s)", join(workingSet)),
305                 new DefaultCollectionPredicate(workingSet) {
306                     @Override
307                     public boolean test(final DocumentName documentName) {
308                         if (includes.matches(documentName)) {
309                             return true;
310                         }
311                         return !excludes.matches(documentName);
312                     }
313                 });
314     }
315 
316     /**
317      * Performs a logical {@code AND} across the collection of matchers.
318      * @param matchers the matchers to check.
319      * @return a matcher that returns {@code true} if all the enclosed matchers return {@code true}.
320      */
321     public static DocumentNameMatcher and(final DocumentNameMatcher... matchers) {
322         return and(Arrays.asList(matchers));
323     }
324 
325     /**
326      * A DocumentName predicate that uses {@link MatchPatterns}.
327      */
328     public static final class MatchPatternsPredicate implements Predicate<DocumentName> {
329         /** The base directory for the pattern matches. */
330         private final DocumentName basedir;
331         /** The pattern matchers. */
332         private final MatchPatterns patterns;
333 
334         private MatchPatternsPredicate(final DocumentName basedir, final MatchPatterns patterns) {
335             this.basedir = basedir;
336             this.patterns = patterns;
337         }
338 
339         @Override
340         public boolean test(final DocumentName documentName) {
341             return patterns.matches(documentName.getName(),
342                     tokenize(documentName.getName(), basedir.getDirectorySeparator()),
343                     basedir.isCaseSensitive());
344         }
345 
346         @Override
347         public String toString() {
348             return patterns.toString();
349         }
350     }
351 
352     /**
353      * A DocumentName predicate that reverses another DocumentNameMatcher.
354      */
355     public static final class NotPredicate implements Predicate<DocumentName> {
356         /** The document name matcher to reverse */
357         private final DocumentNameMatcher nameMatcher;
358 
359         private NotPredicate(final DocumentNameMatcher nameMatcher) {
360             this.nameMatcher = nameMatcher;
361         }
362 
363         @Override
364         public boolean test(final DocumentName documentName) {
365             return !nameMatcher.matches(documentName);
366         }
367 
368         @Override
369         public String toString() {
370             return nameMatcher.predicate.toString();
371         }
372     }
373 
374     /**
375      * A DocumentName predicate that uses {@link FileFilter}.
376      */
377     public static final class FileFilterPredicate implements Predicate<DocumentName> {
378         /** The file filter. */
379         private final FileFilter fileFilter;
380 
381         private FileFilterPredicate(final FileFilter fileFilter) {
382             this.fileFilter = fileFilter;
383         }
384 
385         @Override
386         public boolean test(final DocumentName documentName) {
387             return fileFilter.accept(new File(documentName.getName()));
388         }
389 
390         @Override
391         public String toString() {
392             return fileFilter.toString();
393         }
394     }
395 
396     /**
397      * A marker interface to indicate this predicate contains a collection of matchers.
398      */
399     interface CollectionPredicate extends Predicate<DocumentName> {
400         Iterable<DocumentNameMatcher> getMatchers();
401     }
402     /**
403      * A {@link CollectionPredicate} implementation.
404      */
405     abstract static class DefaultCollectionPredicate implements CollectionPredicate {
406         /** The collection for matchers that make up this predicate */
407         private final Iterable<DocumentNameMatcher> matchers;
408 
409         /**
410          * Constructs a collection predicate from the collection of matchers.
411          * @param matchers the collection of matchers to use.
412          */
413         protected DefaultCollectionPredicate(final Iterable<DocumentNameMatcher> matchers) {
414             this.matchers = matchers;
415         }
416 
417         /**
418          * Gets the internal matchers.
419          * @return an iterable over the internal matchers.
420          */
421         public Iterable<DocumentNameMatcher> getMatchers() {
422             return matchers;
423         }
424 
425         @Override
426         public String toString() {
427             StringBuilder builder = new StringBuilder(this.getClass().getName()).append(": ").append(System.lineSeparator());
428             for (DocumentNameMatcher matcher : matchers) {
429                 builder.append(matcher.predicate.toString()).append(System.lineSeparator());
430             }
431             return builder.toString();
432         }
433     }
434 
435     /**
436      * An implementation of "and" logic across a collection of DocumentNameMatchers.
437      */
438     // package private for testing access
439     static class And extends DefaultCollectionPredicate {
440         And(final Iterable<DocumentNameMatcher> matchers) {
441             super(matchers);
442         }
443 
444         @Override
445         public boolean test(final DocumentName documentName) {
446             for (DocumentNameMatcher matcher : getMatchers()) {
447                 if (!matcher.matches(documentName)) {
448                     return false;
449                 }
450             }
451             return true;
452         }
453     }
454 
455     /**
456      * An implementation of "or" logic across a collection of DocumentNameMatchers.
457      */
458     // package private for testing access
459     static class Or extends DefaultCollectionPredicate {
460         Or(final Iterable<DocumentNameMatcher> matchers) {
461             super(matchers);
462         }
463 
464         @Override
465         public boolean test(final DocumentName documentName) {
466             for (DocumentNameMatcher matcher : getMatchers()) {
467                 if (matcher.matches(documentName)) {
468                     return true;
469                 }
470             }
471             return false;
472         }
473     }
474 
475     /**
476      * Data from a {@link DocumentNameMatcher#decompose(DocumentName)} call.
477      */
478     public static final class DecomposeData {
479         /** The level this data was generated at. */
480         private final int level;
481         /** The name of the DocumentNameMatcher that created this result. */
482         private final DocumentNameMatcher matcher;
483         /** The result of the check. */
484         private final boolean result;
485         /** The candidate */
486         private final DocumentName candidate;
487 
488         private DecomposeData(final int level, final DocumentNameMatcher matcher, final DocumentName candidate, final boolean result) {
489             this.level = level;
490             this.matcher = matcher;
491             this.result = result;
492             this.candidate = candidate;
493         }
494 
495         @Override
496         public String toString() {
497             final String fill = createFill(level);
498             return format("%s%s: >>%s<< %s%n%s",
499                     fill, matcher.toString(), result,
500                     level == 0 ? candidate.getName() : "",
501                     matcher.predicate instanceof CollectionPredicate ?
502                             decompose(level + 1, (CollectionPredicate) matcher.predicate, candidate) :
503                     String.format("%s%s >>%s<<", createFill(level + 1), matcher.predicate.toString(), matcher.predicate.test(candidate)));
504         }
505 
506         private String createFill(final int level) {
507             final char[] chars = new char[level * 2];
508             Arrays.fill(chars, ' ');
509             return new String(chars);
510         }
511 
512         private String decompose(final int level, final CollectionPredicate predicate, final DocumentName candidate) {
513             List<DecomposeData> result = new ArrayList<>();
514 
515             for (DocumentNameMatcher nameMatcher : predicate.getMatchers()) {
516                 nameMatcher.decompose(level, nameMatcher, candidate, result);
517             }
518             StringBuilder sb = new StringBuilder();
519             result.forEach(x -> sb.append(x).append(System.lineSeparator()));
520             return sb.toString();
521         }
522     }
523 }