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