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