001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.utils;
021
022import java.io.Closeable;
023import java.io.File;
024import java.io.IOException;
025import java.lang.reflect.Constructor;
026import java.lang.reflect.InvocationTargetException;
027import java.net.MalformedURLException;
028import java.net.URI;
029import java.net.URISyntaxException;
030import java.net.URL;
031import java.nio.file.Path;
032import java.nio.file.Paths;
033import java.util.regex.Matcher;
034import java.util.regex.Pattern;
035import java.util.regex.PatternSyntaxException;
036
037import org.apache.commons.beanutils.ConversionException;
038
039import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
040
041/**
042 * Contains utility methods.
043 *
044 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
045 */
046public final class CommonUtils {
047
048    /** Prefix for the exception when unable to find resource. */
049    private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
050
051    /** Stop instances being created. **/
052    private CommonUtils() {
053
054    }
055
056    /**
057     * Returns whether the file extension matches what we are meant to process.
058     *
059     * @param file
060     *            the file to be checked.
061     * @param fileExtensions
062     *            files extensions, empty property in config makes it matches to all.
063     * @return whether there is a match.
064     */
065    public static boolean matchesFileExtension(File file, String... fileExtensions) {
066        boolean result = false;
067        if (fileExtensions == null || fileExtensions.length == 0) {
068            result = true;
069        }
070        else {
071            // normalize extensions so all of them have a leading dot
072            final String[] withDotExtensions = new String[fileExtensions.length];
073            for (int i = 0; i < fileExtensions.length; i++) {
074                final String extension = fileExtensions[i];
075                if (startsWithChar(extension, '.')) {
076                    withDotExtensions[i] = extension;
077                }
078                else {
079                    withDotExtensions[i] = "." + extension;
080                }
081            }
082
083            final String fileName = file.getName();
084            for (final String fileExtension : withDotExtensions) {
085                if (fileName.endsWith(fileExtension)) {
086                    result = true;
087                }
088            }
089        }
090
091        return result;
092    }
093
094    /**
095     * Returns whether the specified string contains only whitespace up to the specified index.
096     *
097     * @param index
098     *            index to check up to
099     * @param line
100     *            the line to check
101     * @return whether there is only whitespace
102     */
103    public static boolean hasWhitespaceBefore(int index, String line) {
104        for (int i = 0; i < index; i++) {
105            if (!Character.isWhitespace(line.charAt(i))) {
106                return false;
107            }
108        }
109        return true;
110    }
111
112    /**
113     * Returns the length of a string ignoring all trailing whitespace.
114     * It is a pity that there is not a trim() like
115     * method that only removed the trailing whitespace.
116     *
117     * @param line
118     *            the string to process
119     * @return the length of the string ignoring all trailing whitespace
120     **/
121    public static int lengthMinusTrailingWhitespace(String line) {
122        int len = line.length();
123        for (int i = len - 1; i >= 0; i--) {
124            if (!Character.isWhitespace(line.charAt(i))) {
125                break;
126            }
127            len--;
128        }
129        return len;
130    }
131
132    /**
133     * Returns the length of a String prefix with tabs expanded.
134     * Each tab is counted as the number of characters is
135     * takes to jump to the next tab stop.
136     *
137     * @param inputString
138     *            the input String
139     * @param toIdx
140     *            index in string (exclusive) where the calculation stops
141     * @param tabWidth
142     *            the distance between tab stop position.
143     * @return the length of string.substring(0, toIdx) with tabs expanded.
144     */
145    public static int lengthExpandedTabs(String inputString,
146            int toIdx,
147            int tabWidth) {
148        int len = 0;
149        for (int idx = 0; idx < toIdx; idx++) {
150            if (inputString.charAt(idx) == '\t') {
151                len = (len / tabWidth + 1) * tabWidth;
152            }
153            else {
154                len++;
155            }
156        }
157        return len;
158    }
159
160    /**
161     * Validates whether passed string is a valid pattern or not.
162     *
163     * @param pattern
164     *            string to validate
165     * @return true if the pattern is valid false otherwise
166     */
167    public static boolean isPatternValid(String pattern) {
168        try {
169            Pattern.compile(pattern);
170        }
171        catch (final PatternSyntaxException ignored) {
172            return false;
173        }
174        return true;
175    }
176
177    /**
178     * Helper method to create a regular expression.
179     *
180     * @param pattern
181     *            the pattern to match
182     * @return a created regexp object
183     * @throws ConversionException
184     *             if unable to create Pattern object.
185     **/
186    public static Pattern createPattern(String pattern) {
187        return createPattern(pattern, 0);
188    }
189
190    /**
191     * Helper method to create a regular expression with a specific flags.
192     *
193     * @param pattern
194     *            the pattern to match
195     * @param flags
196     *            the flags to set
197     * @return a created regexp object
198     * @throws ConversionException
199     *             if unable to create Pattern object.
200     **/
201    public static Pattern createPattern(String pattern, int flags) {
202        try {
203            return Pattern.compile(pattern, flags);
204        }
205        catch (final PatternSyntaxException e) {
206            throw new ConversionException(
207                    "Failed to initialise regular expression " + pattern, e);
208        }
209    }
210
211    /**
212     * @param type
213     *            the fully qualified name. Cannot be null
214     * @return the base class name from a fully qualified name
215     */
216    public static String baseClassName(String type) {
217        final int index = type.lastIndexOf('.');
218
219        if (index == -1) {
220            return type;
221        }
222        else {
223            return type.substring(index + 1);
224        }
225    }
226
227    /**
228     * Constructs a normalized relative path between base directory and a given path.
229     *
230     * @param baseDirectory
231     *            the base path to which given path is relativized
232     * @param path
233     *            the path to relativize against base directory
234     * @return the relative normalized path between base directory and
235     *     path or path if base directory is null.
236     */
237    public static String relativizeAndNormalizePath(final String baseDirectory, final String path) {
238        if (baseDirectory == null) {
239            return path;
240        }
241        final Path pathAbsolute = Paths.get(path).normalize();
242        final Path pathBase = Paths.get(baseDirectory).normalize();
243        return pathBase.relativize(pathAbsolute).toString();
244    }
245
246    /**
247     * Tests if this string starts with the specified prefix.
248     * <p>
249     * It is faster version of {@link String#startsWith(String)} optimized for
250     *  one-character prefixes at the expense of
251     * some readability. Suggested by SimplifyStartsWith PMD rule:
252     * http://pmd.sourceforge.net/pmd-5.3.1/pmd-java/rules/java/optimizations.html#SimplifyStartsWith
253     * </p>
254     *
255     * @param value
256     *            the {@code String} to check
257     * @param prefix
258     *            the prefix to find
259     * @return {@code true} if the {@code char} is a prefix of the given {@code String};
260     *  {@code false} otherwise.
261     */
262    public static boolean startsWithChar(String value, char prefix) {
263        return !value.isEmpty() && value.charAt(0) == prefix;
264    }
265
266    /**
267     * Tests if this string ends with the specified suffix.
268     * <p>
269     * It is faster version of {@link String#endsWith(String)} optimized for
270     *  one-character suffixes at the expense of
271     * some readability. Suggested by SimplifyStartsWith PMD rule:
272     * http://pmd.sourceforge.net/pmd-5.3.1/pmd-java/rules/java/optimizations.html#SimplifyStartsWith
273     * </p>
274     *
275     * @param value
276     *            the {@code String} to check
277     * @param suffix
278     *            the suffix to find
279     * @return {@code true} if the {@code char} is a suffix of the given {@code String};
280     *  {@code false} otherwise.
281     */
282    public static boolean endsWithChar(String value, char suffix) {
283        return !value.isEmpty() && value.charAt(value.length() - 1) == suffix;
284    }
285
286    /**
287     * Gets constructor of targetClass.
288     * @param targetClass
289     *            from which constructor is returned
290     * @param parameterTypes
291     *            of constructor
292     * @return constructor of targetClass or {@link IllegalStateException} if any exception occurs
293     * @see Class#getConstructor(Class[])
294     */
295    public static Constructor<?> getConstructor(Class<?> targetClass, Class<?>... parameterTypes) {
296        try {
297            return targetClass.getConstructor(parameterTypes);
298        }
299        catch (NoSuchMethodException ex) {
300            throw new IllegalStateException(ex);
301        }
302    }
303
304    /**
305     * @param constructor
306     *            to invoke
307     * @param parameters
308     *            to pass to constructor
309     * @param <T>
310     *            type of constructor
311     * @return new instance of class or {@link IllegalStateException} if any exception occurs
312     * @see Constructor#newInstance(Object...)
313     */
314    public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) {
315        try {
316            return constructor.newInstance(parameters);
317        }
318        catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
319            throw new IllegalStateException(ex);
320        }
321    }
322
323    /**
324     * Closes a stream re-throwing IOException as IllegalStateException.
325     *
326     * @param closeable
327     *            Closeable object
328     */
329    public static void close(Closeable closeable) {
330        if (closeable == null) {
331            return;
332        }
333        try {
334            closeable.close();
335        }
336        catch (IOException e) {
337            throw new IllegalStateException("Cannot close the stream", e);
338        }
339    }
340
341    /**
342     * Resolve the specified filename to a URI.
343     * @param filename name os the file
344     * @return resolved header file URI
345     * @throws CheckstyleException on failure
346     */
347    public static URI getUriByFilename(String filename) throws CheckstyleException {
348        // figure out if this is a File or a URL
349        URI uri;
350        try {
351            final URL url = new URL(filename);
352            uri = url.toURI();
353        }
354        catch (final URISyntaxException | MalformedURLException ignored) {
355            uri = null;
356        }
357
358        if (uri == null) {
359            final File file = new File(filename);
360            if (file.exists()) {
361                uri = file.toURI();
362            }
363            else {
364                // check to see if the file is in the classpath
365                try {
366                    final URL configUrl = CommonUtils.class
367                            .getResource(filename);
368                    if (configUrl == null) {
369                        throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
370                    }
371                    uri = configUrl.toURI();
372                }
373                catch (final URISyntaxException e) {
374                    throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, e);
375                }
376            }
377        }
378
379        return uri;
380    }
381
382    /**
383     * Puts part of line, which matches regexp into given template
384     * on positions $n where 'n' is number of matched part in line.
385     * @param template the string to expand.
386     * @param lineToPlaceInTemplate contains expression which should be placed into string.
387     * @param regexp expression to find in comment.
388     * @return the string, based on template filled with given lines
389     */
390    public static String fillTemplateWithStringsByRegexp(
391        String template, String lineToPlaceInTemplate, Pattern regexp) {
392        final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
393        String result = template;
394        if (matcher.find()) {
395            for (int i = 0; i <= matcher.groupCount(); i++) {
396                // $n expands comment match like in Pattern.subst().
397                result = result.replaceAll("\\$" + i, matcher.group(i));
398            }
399        }
400        return result;
401    }
402}