001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2016 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     * Helper method to create a regular expression.
058     *
059     * @param pattern
060     *            the pattern to match
061     * @return a created regexp object
062     * @throws ConversionException
063     *             if unable to create Pattern object.
064     **/
065    public static Pattern createPattern(String pattern) {
066        return createPattern(pattern, 0);
067    }
068
069    /**
070     * Helper method to create a regular expression with a specific flags.
071     *
072     * @param pattern
073     *            the pattern to match
074     * @param flags
075     *            the flags to set
076     * @return a created regexp object
077     * @throws ConversionException
078     *             if unable to create Pattern object.
079     **/
080    public static Pattern createPattern(String pattern, int flags) {
081        try {
082            return Pattern.compile(pattern, flags);
083        }
084        catch (final PatternSyntaxException ex) {
085            throw new ConversionException(
086                "Failed to initialise regular expression " + pattern, ex);
087        }
088    }
089
090    /**
091     * Returns whether the file extension matches what we are meant to process.
092     *
093     * @param file
094     *            the file to be checked.
095     * @param fileExtensions
096     *            files extensions, empty property in config makes it matches to all.
097     * @return whether there is a match.
098     */
099    public static boolean matchesFileExtension(File file, String... fileExtensions) {
100        boolean result = false;
101        if (fileExtensions == null || fileExtensions.length == 0) {
102            result = true;
103        }
104        else {
105            // normalize extensions so all of them have a leading dot
106            final String[] withDotExtensions = new String[fileExtensions.length];
107            for (int i = 0; i < fileExtensions.length; i++) {
108                final String extension = fileExtensions[i];
109                if (startsWithChar(extension, '.')) {
110                    withDotExtensions[i] = extension;
111                }
112                else {
113                    withDotExtensions[i] = "." + extension;
114                }
115            }
116
117            final String fileName = file.getName();
118            for (final String fileExtension : withDotExtensions) {
119                if (fileName.endsWith(fileExtension)) {
120                    result = true;
121                }
122            }
123        }
124
125        return result;
126    }
127
128    /**
129     * Returns whether the specified string contains only whitespace up to the specified index.
130     *
131     * @param index
132     *            index to check up to
133     * @param line
134     *            the line to check
135     * @return whether there is only whitespace
136     */
137    public static boolean hasWhitespaceBefore(int index, String line) {
138        for (int i = 0; i < index; i++) {
139            if (!Character.isWhitespace(line.charAt(i))) {
140                return false;
141            }
142        }
143        return true;
144    }
145
146    /**
147     * Returns the length of a string ignoring all trailing whitespace.
148     * It is a pity that there is not a trim() like
149     * method that only removed the trailing whitespace.
150     *
151     * @param line
152     *            the string to process
153     * @return the length of the string ignoring all trailing whitespace
154     **/
155    public static int lengthMinusTrailingWhitespace(String line) {
156        int len = line.length();
157        for (int i = len - 1; i >= 0; i--) {
158            if (!Character.isWhitespace(line.charAt(i))) {
159                break;
160            }
161            len--;
162        }
163        return len;
164    }
165
166    /**
167     * Returns the length of a String prefix with tabs expanded.
168     * Each tab is counted as the number of characters is
169     * takes to jump to the next tab stop.
170     *
171     * @param inputString
172     *            the input String
173     * @param toIdx
174     *            index in string (exclusive) where the calculation stops
175     * @param tabWidth
176     *            the distance between tab stop position.
177     * @return the length of string.substring(0, toIdx) with tabs expanded.
178     */
179    public static int lengthExpandedTabs(String inputString,
180            int toIdx,
181            int tabWidth) {
182        int len = 0;
183        for (int idx = 0; idx < toIdx; idx++) {
184            if (inputString.charAt(idx) == '\t') {
185                len = (len / tabWidth + 1) * tabWidth;
186            }
187            else {
188                len++;
189            }
190        }
191        return len;
192    }
193
194    /**
195     * Validates whether passed string is a valid pattern or not.
196     *
197     * @param pattern
198     *            string to validate
199     * @return true if the pattern is valid false otherwise
200     */
201    public static boolean isPatternValid(String pattern) {
202        try {
203            Pattern.compile(pattern);
204        }
205        catch (final PatternSyntaxException ignored) {
206            return false;
207        }
208        return true;
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     * @param <T> type of the target class object.
293     * @return constructor of targetClass or {@link IllegalStateException} if any exception occurs
294     * @see Class#getConstructor(Class[])
295     */
296    public static <T> Constructor<T> getConstructor(Class<T> targetClass,
297                                                    Class<?>... parameterTypes) {
298        try {
299            return targetClass.getConstructor(parameterTypes);
300        }
301        catch (NoSuchMethodException ex) {
302            throw new IllegalStateException(ex);
303        }
304    }
305
306    /**
307     * @param constructor
308     *            to invoke
309     * @param parameters
310     *            to pass to constructor
311     * @param <T>
312     *            type of constructor
313     * @return new instance of class or {@link IllegalStateException} if any exception occurs
314     * @see Constructor#newInstance(Object...)
315     */
316    public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) {
317        try {
318            return constructor.newInstance(parameters);
319        }
320        catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
321            throw new IllegalStateException(ex);
322        }
323    }
324
325    /**
326     * Closes a stream re-throwing IOException as IllegalStateException.
327     *
328     * @param closeable
329     *            Closeable object
330     */
331    public static void close(Closeable closeable) {
332        if (closeable == null) {
333            return;
334        }
335        try {
336            closeable.close();
337        }
338        catch (IOException ex) {
339            throw new IllegalStateException("Cannot close the stream", ex);
340        }
341    }
342
343    /**
344     * Resolve the specified filename to a URI.
345     * @param filename name os the file
346     * @return resolved header file URI
347     * @throws CheckstyleException on failure
348     */
349    public static URI getUriByFilename(String filename) throws CheckstyleException {
350        // figure out if this is a File or a URL
351        URI uri;
352        try {
353            final URL url = new URL(filename);
354            uri = url.toURI();
355        }
356        catch (final URISyntaxException | MalformedURLException ignored) {
357            uri = null;
358        }
359
360        if (uri == null) {
361            final File file = new File(filename);
362            if (file.exists()) {
363                uri = file.toURI();
364            }
365            else {
366                // check to see if the file is in the classpath
367                try {
368                    final URL configUrl = CommonUtils.class
369                            .getResource(filename);
370                    if (configUrl == null) {
371                        throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
372                    }
373                    uri = configUrl.toURI();
374                }
375                catch (final URISyntaxException ex) {
376                    throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, ex);
377                }
378            }
379        }
380
381        return uri;
382    }
383
384    /**
385     * Puts part of line, which matches regexp into given template
386     * on positions $n where 'n' is number of matched part in line.
387     * @param template the string to expand.
388     * @param lineToPlaceInTemplate contains expression which should be placed into string.
389     * @param regexp expression to find in comment.
390     * @return the string, based on template filled with given lines
391     */
392    public static String fillTemplateWithStringsByRegexp(
393        String template, String lineToPlaceInTemplate, Pattern regexp) {
394        final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
395        String result = template;
396        if (matcher.find()) {
397            for (int i = 0; i <= matcher.groupCount(); i++) {
398                // $n expands comment match like in Pattern.subst().
399                result = result.replaceAll("\\$" + i, matcher.group(i));
400            }
401        }
402        return result;
403    }
404}