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.File;
22  import java.io.FileFilter;
23  import java.io.FileNotFoundException;
24  import java.io.FileReader;
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Objects;
31  import java.util.function.Predicate;
32  
33  import org.apache.commons.io.IOUtils;
34  import org.apache.commons.io.LineIterator;
35  import org.apache.commons.lang3.StringUtils;
36  import org.apache.rat.ConfigurationException;
37  import org.apache.rat.api.EnvVar;
38  import org.apache.rat.config.exclusion.plexus.MatchPattern;
39  import org.apache.rat.config.exclusion.plexus.SelectorUtils;
40  import org.apache.rat.document.DocumentName;
41  import org.apache.rat.document.DocumentNameMatcher;
42  import org.apache.rat.utils.ExtendedIterator;
43  
44  import static java.lang.String.format;
45  
46  /**
47   * Utilities for exclusion processing.
48   */
49  public final class ExclusionUtils {
50  
51      /** The list of comment prefixes that are used to filter comment lines.  */
52      public static final List<String> COMMENT_PREFIXES = Arrays.asList("#", "##", "//", "/**", "/*");
53  
54      /** Prefix used to negate a given pattern. */
55      public static final String NEGATION_PREFIX = "!";
56  
57      /** A predicate that filters out lines that do NOT start with {@link #NEGATION_PREFIX}. */
58      public static final Predicate<String> NOT_MATCH_FILTER = s -> s.startsWith(NEGATION_PREFIX);
59  
60      /** A predicate that filters out lines that start with {@link #NEGATION_PREFIX}. */
61      public static final Predicate<String> MATCH_FILTER = NOT_MATCH_FILTER.negate();
62  
63      private ExclusionUtils() {
64          // do not instantiate
65      }
66  
67      /**
68       * Creates predicate that filters out comment and blank lines. Leading spaces are removed and
69       * if the line then starts with a commentPrefix string it is considered a comment and will be removed
70       *
71       * @param commentPrefixes the list of comment prefixes.
72       * @return the Predicate that returns false for lines that start with commentPrefixes or are empty.
73       */
74      public static Predicate<String> commentFilter(final Iterable<String> commentPrefixes) {
75          return s -> {
76              if (StringUtils.isNotBlank(s)) {
77                  int i = 1;
78                  while (StringUtils.isBlank(s.substring(0, i))) {
79                      i++;
80                  }
81                  String trimmed = i > 0 ? s.substring(i - 1) : s;
82                  for (String prefix : commentPrefixes) {
83                      if (trimmed.startsWith(prefix)) {
84                          return false;
85                      }
86                  }
87                  return true;
88              }
89              return false;
90          };
91      }
92  
93      /**
94       * Creates predicate that filters out comment and blank lines. Leading spaces are removed and
95       * if the line then starts with a commentPrefix string it is considered a comment and will be removed
96       *
97       * @param commentPrefix the prefix string for comments.
98       * @return the Predicate that returns false for lines that start with commentPrefixes or are empty.
99       */
100     public static Predicate<String> commentFilter(final String commentPrefix) {
101         return s -> {
102             if (StringUtils.isNotBlank(s)) {
103                 int i = 1;
104                 while (StringUtils.isBlank(s.substring(0, i))) {
105                     i++;
106                 }
107                 String trimmed = i > 0 ? s.substring(i - 1) : s;
108                 return !trimmed.startsWith(commentPrefix);
109             }
110             return false;
111         };
112     }
113 
114     /**
115      * Create a FileFilter from a PathMatcher.
116      * @param parent the document name for the parent of the file to be filtered.
117      * @param nameMatcher the path matcher to convert.
118      * @return a FileFilter.
119      */
120     public static FileFilter asFileFilter(final DocumentName parent, final DocumentNameMatcher nameMatcher) {
121         return file -> {
122             DocumentName candidate = DocumentName.builder(file).setBaseName(parent.getBaseName()).build();
123             return EnvVar.RAT_DECOMPOSE_MATCHER_ON_USE.isSet() ? nameMatcher.logDecompositionWhileMatching(candidate) :
124                 nameMatcher.matches(candidate);
125         };
126     }
127 
128     /**
129      * Creates an iterator of Strings from a file of patterns.
130      * Removes comment lines.
131      * @param patternFile the file to read.
132      * @param commentFilters A predicate returning {@code true} for non-comment lines.
133      * @return the iterable of Strings from the file.
134      */
135     public static ExtendedIterator<String> asIterator(final File patternFile, final Predicate<String> commentFilters) {
136         verifyFile(patternFile);
137         Objects.requireNonNull(commentFilters, "commentFilters");
138         try {
139             return ExtendedIterator.create(IOUtils.lineIterator(new FileReader(patternFile))).filter(commentFilters);
140         } catch (FileNotFoundException e) {
141             throw new ConfigurationException(format("%s is not a valid file.", patternFile));
142         }
143     }
144 
145     /**
146      * Creates an iterable of Strings from a file of patterns.
147      * Removes comment lines.
148      * @param patternFile the file to read.
149      * @param commentPrefix the prefix string for comments.
150      * @return the iterable of Strings from the file.
151      */
152     public static Iterable<String> asIterable(final File patternFile, final String commentPrefix)  {
153         return asIterable(patternFile, commentFilter(commentPrefix));
154     }
155 
156     /**
157      * Creates an iterable of Strings from a file of patterns.
158      * Removes comment lines.
159      * @param patternFile the file to read.
160      * @param commentFilters A predicate returning {@code true} for non-comment lines.
161      * @return the iterable of Strings from the file.
162      */
163     public static Iterable<String> asIterable(final File patternFile, final Predicate<String> commentFilters)  {
164         verifyFile(patternFile);
165         Objects.requireNonNull(commentFilters, "commentFilters");
166         // can not return LineIterator directly as the patternFile will not be closed leading
167         // to a resource leak in some cases.
168         try (FileReader reader = new FileReader(patternFile)) {
169             List<String> result = new ArrayList<>();
170             Iterator<String> iter = new LineIterator(reader) {
171                 @Override
172                 protected boolean isValidLine(final String line) {
173                     return commentFilters.test(line);
174                 }
175             };
176             iter.forEachRemaining(result::add);
177             return result;
178         } catch (IOException e) {
179             throw new ConfigurationException("Unable to read file " + patternFile, e);
180         }
181     }
182 
183     /**
184      * Returns {@code true} if the filename represents a hidden file
185      * @param fileName the file to check.
186      * @return true if it is the name of a hidden file.
187      */
188     public static boolean isHidden(final String fileName) {
189         return fileName.startsWith(".") && !(fileName.equals(".") || fileName.equals(".."));
190     }
191 
192     private static void verifyFile(final File file) {
193         if (file == null || !file.exists() || !file.isFile()) {
194             throw new ConfigurationException(format("%s is not a valid file.", file));
195         }
196     }
197 
198     /**
199      * Modifies the {@link MatchPattern} formatted {@code pattern} argument by expanding the pattern and
200      * by adjusting the pattern to include the basename from the {@code documentName} argument.
201      * @param documentName the name of the file being read.
202      * @param pattern the pattern to format.
203      * @return the completely formatted pattern
204      */
205     public static String qualifyPattern(final DocumentName documentName, final String pattern) {
206         boolean prefix = pattern.startsWith(NEGATION_PREFIX);
207         String workingPattern = prefix ? pattern.substring(1) : pattern;
208         String normalizedPattern = SelectorUtils.extractPattern(workingPattern, documentName.getDirectorySeparator());
209 
210         StringBuilder sb = new StringBuilder(prefix ? NEGATION_PREFIX : "");
211         if (SelectorUtils.isRegexPrefixedPattern(workingPattern)) {
212             sb.append(SelectorUtils.REGEX_HANDLER_PREFIX)
213                     .append("\\Q").append(documentName.getBaseName())
214                     .append(documentName.getDirectorySeparator())
215                     .append("\\E").append(normalizedPattern)
216                     .append(SelectorUtils.PATTERN_HANDLER_SUFFIX);
217         } else {
218             sb.append(documentName.getBaseDocumentName().resolve(normalizedPattern).getName());
219         }
220         return sb.toString();
221     }
222 
223     /**
224      * Tokenizes the string based on the directory separator.
225      * @param source the source to tokenize.
226      * @param from the directory separator for the source.
227      * @param to the directory separator for the result.
228      * @return the source string with the separators converted.
229      */
230     public static String convertSeparator(final String source, final String from, final String to) {
231         if (StringUtils.isEmpty(source) || from.equals(to)) {
232             return source;
233         }
234         return String.join(to, source.split("\\Q" + from + "\\E"));
235     }
236 }