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