001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2022 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.api;
021
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.io.Reader;
025import java.io.Serializable;
026import java.net.URL;
027import java.net.URLConnection;
028import java.nio.charset.StandardCharsets;
029import java.text.MessageFormat;
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.Locale;
034import java.util.Map;
035import java.util.MissingResourceException;
036import java.util.Objects;
037import java.util.PropertyResourceBundle;
038import java.util.ResourceBundle;
039import java.util.ResourceBundle.Control;
040
041/**
042 * Represents a violation that can be localised. The translations come from
043 * message.properties files. The underlying implementation uses
044 * java.text.MessageFormat.
045 *
046 * @noinspection SerializableHasSerializationMethods, ClassWithTooManyConstructors
047 */
048public final class Violation
049    implements Comparable<Violation>, Serializable {
050
051    /** A unique serial version identifier. */
052    private static final long serialVersionUID = 5675176836184862150L;
053
054    /**
055     * A cache that maps bundle names to ResourceBundles.
056     * Avoids repetitive calls to ResourceBundle.getBundle().
057     */
058    private static final Map<String, ResourceBundle> BUNDLE_CACHE =
059        Collections.synchronizedMap(new HashMap<>());
060
061    /** The default severity level if one is not specified. */
062    private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
063
064    /** The locale to localise violations to. **/
065    private static Locale sLocale = Locale.getDefault();
066
067    /** The line number. **/
068    private final int lineNo;
069    /** The column number. **/
070    private final int columnNo;
071    /** The column char index. **/
072    private final int columnCharIndex;
073    /** The token type constant. See {@link TokenTypes}. **/
074    private final int tokenType;
075
076    /** The severity level. **/
077    private final SeverityLevel severityLevel;
078
079    /** The id of the module generating the violation. */
080    private final String moduleId;
081
082    /** Key for the violation format. **/
083    private final String key;
084
085    /**
086     * Arguments for MessageFormat.
087     *
088     * @noinspection NonSerializableFieldInSerializableClass
089     */
090    private final Object[] args;
091
092    /** Name of the resource bundle to get violations from. **/
093    private final String bundle;
094
095    /** Class of the source for this Violation. */
096    private final Class<?> sourceClass;
097
098    /** A custom violation overriding the default violation from the bundle. */
099    private final String customMessage;
100
101    /**
102     * Creates a new {@code Violation} instance.
103     *
104     * @param lineNo line number associated with the violation
105     * @param columnNo column number associated with the violation
106     * @param columnCharIndex column char index associated with the violation
107     * @param tokenType token type of the event associated with violation. See {@link TokenTypes}
108     * @param bundle resource bundle name
109     * @param key the key to locate the translation
110     * @param args arguments for the translation
111     * @param severityLevel severity level for the violation
112     * @param moduleId the id of the module the violation is associated with
113     * @param sourceClass the Class that is the source of the violation
114     * @param customMessage optional custom violation overriding the default
115     * @noinspection ConstructorWithTooManyParameters
116     */
117    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
118    public Violation(int lineNo,
119                            int columnNo,
120                            int columnCharIndex,
121                            int tokenType,
122                            String bundle,
123                            String key,
124                            Object[] args,
125                            SeverityLevel severityLevel,
126                            String moduleId,
127                            Class<?> sourceClass,
128                            String customMessage) {
129        this.lineNo = lineNo;
130        this.columnNo = columnNo;
131        this.columnCharIndex = columnCharIndex;
132        this.tokenType = tokenType;
133        this.key = key;
134
135        if (args == null) {
136            this.args = null;
137        }
138        else {
139            this.args = Arrays.copyOf(args, args.length);
140        }
141        this.bundle = bundle;
142        this.severityLevel = severityLevel;
143        this.moduleId = moduleId;
144        this.sourceClass = sourceClass;
145        this.customMessage = customMessage;
146    }
147
148    /**
149     * Creates a new {@code Violation} instance.
150     *
151     * @param lineNo line number associated with the violation
152     * @param columnNo column number associated with the violation
153     * @param tokenType token type of the event associated with violation. See {@link TokenTypes}
154     * @param bundle resource bundle name
155     * @param key the key to locate the translation
156     * @param args arguments for the translation
157     * @param severityLevel severity level for the violation
158     * @param moduleId the id of the module the violation is associated with
159     * @param sourceClass the Class that is the source of the violation
160     * @param customMessage optional custom violation overriding the default
161     * @noinspection ConstructorWithTooManyParameters
162     */
163    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
164    public Violation(int lineNo,
165                            int columnNo,
166                            int tokenType,
167                            String bundle,
168                            String key,
169                            Object[] args,
170                            SeverityLevel severityLevel,
171                            String moduleId,
172                            Class<?> sourceClass,
173                            String customMessage) {
174        this(lineNo, columnNo, columnNo, tokenType, bundle, key, args, severityLevel, moduleId,
175                sourceClass, customMessage);
176    }
177
178    /**
179     * Creates a new {@code Violation} instance.
180     *
181     * @param lineNo line number associated with the violation
182     * @param columnNo column number associated with the violation
183     * @param bundle resource bundle name
184     * @param key the key to locate the translation
185     * @param args arguments for the translation
186     * @param severityLevel severity level for the violation
187     * @param moduleId the id of the module the violation is associated with
188     * @param sourceClass the Class that is the source of the violation
189     * @param customMessage optional custom violation overriding the default
190     * @noinspection ConstructorWithTooManyParameters
191     */
192    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
193    public Violation(int lineNo,
194                            int columnNo,
195                            String bundle,
196                            String key,
197                            Object[] args,
198                            SeverityLevel severityLevel,
199                            String moduleId,
200                            Class<?> sourceClass,
201                            String customMessage) {
202        this(lineNo, columnNo, 0, bundle, key, args, severityLevel, moduleId, sourceClass,
203                customMessage);
204    }
205
206    /**
207     * Creates a new {@code Violation} instance.
208     *
209     * @param lineNo line number associated with the violation
210     * @param columnNo column number associated with the violation
211     * @param bundle resource bundle name
212     * @param key the key to locate the translation
213     * @param args arguments for the translation
214     * @param moduleId the id of the module the violation is associated with
215     * @param sourceClass the Class that is the source of the violation
216     * @param customMessage optional custom violation overriding the default
217     * @noinspection ConstructorWithTooManyParameters
218     */
219    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
220    public Violation(int lineNo,
221                            int columnNo,
222                            String bundle,
223                            String key,
224                            Object[] args,
225                            String moduleId,
226                            Class<?> sourceClass,
227                            String customMessage) {
228        this(lineNo,
229                columnNo,
230             bundle,
231             key,
232             args,
233             DEFAULT_SEVERITY,
234             moduleId,
235             sourceClass,
236             customMessage);
237    }
238
239    /**
240     * Creates a new {@code Violation} instance.
241     *
242     * @param lineNo line number associated with the violation
243     * @param bundle resource bundle name
244     * @param key the key to locate the translation
245     * @param args arguments for the translation
246     * @param severityLevel severity level for the violation
247     * @param moduleId the id of the module the violation is associated with
248     * @param sourceClass the source class for the violation
249     * @param customMessage optional custom violation overriding the default
250     * @noinspection ConstructorWithTooManyParameters
251     */
252    // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
253    public Violation(int lineNo,
254                            String bundle,
255                            String key,
256                            Object[] args,
257                            SeverityLevel severityLevel,
258                            String moduleId,
259                            Class<?> sourceClass,
260                            String customMessage) {
261        this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
262                sourceClass, customMessage);
263    }
264
265    /**
266     * Creates a new {@code Violation} instance. The column number
267     * defaults to 0.
268     *
269     * @param lineNo line number associated with the violation
270     * @param bundle name of a resource bundle that contains audit event violations
271     * @param key the key to locate the translation
272     * @param args arguments for the translation
273     * @param moduleId the id of the module the violation is associated with
274     * @param sourceClass the name of the source for the violation
275     * @param customMessage optional custom violation overriding the default
276     */
277    public Violation(
278        int lineNo,
279        String bundle,
280        String key,
281        Object[] args,
282        String moduleId,
283        Class<?> sourceClass,
284        String customMessage) {
285        this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
286                sourceClass, customMessage);
287    }
288
289    /**
290     * Gets the line number.
291     *
292     * @return the line number
293     */
294    public int getLineNo() {
295        return lineNo;
296    }
297
298    /**
299     * Gets the column number.
300     *
301     * @return the column number
302     */
303    public int getColumnNo() {
304        return columnNo;
305    }
306
307    /**
308     * Gets the column char index.
309     *
310     * @return the column char index
311     */
312    public int getColumnCharIndex() {
313        return columnCharIndex;
314    }
315
316    /**
317     * Gets the token type.
318     *
319     * @return the token type
320     */
321    public int getTokenType() {
322        return tokenType;
323    }
324
325    /**
326     * Gets the severity level.
327     *
328     * @return the severity level
329     */
330    public SeverityLevel getSeverityLevel() {
331        return severityLevel;
332    }
333
334    /**
335     * Returns id of module.
336     *
337     * @return the module identifier.
338     */
339    public String getModuleId() {
340        return moduleId;
341    }
342
343    /**
344     * Returns the violation key to locate the translation, can also be used
345     * in IDE plugins to map audit event violations to corrective actions.
346     *
347     * @return the violation key
348     */
349    public String getKey() {
350        return key;
351    }
352
353    /**
354     * Gets the name of the source for this Violation.
355     *
356     * @return the name of the source for this Violation
357     */
358    public String getSourceName() {
359        return sourceClass.getName();
360    }
361
362    /**
363     * Sets a locale to use for localization.
364     *
365     * @param locale the locale to use for localization
366     */
367    public static void setLocale(Locale locale) {
368        clearCache();
369        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
370            sLocale = Locale.ROOT;
371        }
372        else {
373            sLocale = locale;
374        }
375    }
376
377    /** Clears the cache. */
378    public static void clearCache() {
379        BUNDLE_CACHE.clear();
380    }
381
382    /**
383     * Indicates whether some other object is "equal to" this one.
384     * Suppression on enumeration is needed so code stays consistent.
385     *
386     * @noinspection EqualsCalledOnEnumConstant
387     */
388    // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
389    @Override
390    public boolean equals(Object object) {
391        if (this == object) {
392            return true;
393        }
394        if (object == null || getClass() != object.getClass()) {
395            return false;
396        }
397        final Violation violation = (Violation) object;
398        return Objects.equals(lineNo, violation.lineNo)
399                && Objects.equals(columnNo, violation.columnNo)
400                && Objects.equals(columnCharIndex, violation.columnCharIndex)
401                && Objects.equals(tokenType, violation.tokenType)
402                && Objects.equals(severityLevel, violation.severityLevel)
403                && Objects.equals(moduleId, violation.moduleId)
404                && Objects.equals(key, violation.key)
405                && Objects.equals(bundle, violation.bundle)
406                && Objects.equals(sourceClass, violation.sourceClass)
407                && Objects.equals(customMessage, violation.customMessage)
408                && Arrays.equals(args, violation.args);
409    }
410
411    @Override
412    public int hashCode() {
413        return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
414                key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
415    }
416
417    ////////////////////////////////////////////////////////////////////////////
418    // Interface Comparable methods
419    ////////////////////////////////////////////////////////////////////////////
420
421    @Override
422    public int compareTo(Violation other) {
423        final int result;
424
425        if (lineNo == other.lineNo) {
426            if (columnNo == other.columnNo) {
427                if (Objects.equals(moduleId, other.moduleId)) {
428                    result = getViolation().compareTo(other.getViolation());
429                }
430                else if (moduleId == null) {
431                    result = -1;
432                }
433                else if (other.moduleId == null) {
434                    result = 1;
435                }
436                else {
437                    result = moduleId.compareTo(other.moduleId);
438                }
439            }
440            else {
441                result = Integer.compare(columnNo, other.columnNo);
442            }
443        }
444        else {
445            result = Integer.compare(lineNo, other.lineNo);
446        }
447        return result;
448    }
449
450    /**
451     * Gets the translated violation.
452     *
453     * @return the translated violation
454     */
455    public String getViolation() {
456        String violation = getCustomViolation();
457
458        if (violation == null) {
459            try {
460                // Important to use the default class loader, and not the one in
461                // the GlobalProperties object. This is because the class loader in
462                // the GlobalProperties is specified by the user for resolving
463                // custom classes.
464                final ResourceBundle resourceBundle = getBundle(bundle);
465                final String pattern = resourceBundle.getString(key);
466                final MessageFormat formatter = new MessageFormat(pattern, Locale.ROOT);
467                violation = formatter.format(args);
468            }
469            catch (final MissingResourceException ignored) {
470                // If the Check author didn't provide i18n resource bundles
471                // and logs audit event violations directly, this will return
472                // the author's original violation
473                final MessageFormat formatter = new MessageFormat(key, Locale.ROOT);
474                violation = formatter.format(args);
475            }
476        }
477        return violation;
478    }
479
480    /**
481     * Returns the formatted custom violation if one is configured.
482     *
483     * @return the formatted custom violation or {@code null}
484     *          if there is no custom violation
485     */
486    private String getCustomViolation() {
487        String violation = null;
488        if (customMessage != null) {
489            final MessageFormat formatter = new MessageFormat(customMessage, Locale.ROOT);
490            violation = formatter.format(args);
491        }
492        return violation;
493    }
494
495    /**
496     * Find a ResourceBundle for a given bundle name. Uses the classloader
497     * of the class emitting this violation, to be sure to get the correct
498     * bundle.
499     *
500     * @param bundleName the bundle name
501     * @return a ResourceBundle
502     */
503    private ResourceBundle getBundle(String bundleName) {
504        return BUNDLE_CACHE.computeIfAbsent(bundleName, name -> {
505            return ResourceBundle.getBundle(
506                name, sLocale, sourceClass.getClassLoader(), new Utf8Control());
507        });
508    }
509
510    /**
511     * <p>
512     * Custom ResourceBundle.Control implementation which allows explicitly read
513     * the properties files as UTF-8.
514     * </p>
515     */
516    public static class Utf8Control extends Control {
517
518        @Override
519        public ResourceBundle newBundle(String baseName, Locale locale, String format,
520                 ClassLoader loader, boolean reload) throws IOException {
521            // The below is a copy of the default implementation.
522            final String bundleName = toBundleName(baseName, locale);
523            final String resourceName = toResourceName(bundleName, "properties");
524            final URL url = loader.getResource(resourceName);
525            ResourceBundle resourceBundle = null;
526            if (url != null) {
527                final URLConnection connection = url.openConnection();
528                if (connection != null) {
529                    connection.setUseCaches(!reload);
530                    try (Reader streamReader = new InputStreamReader(connection.getInputStream(),
531                            StandardCharsets.UTF_8)) {
532                        // Only this line is changed to make it read property files as UTF-8.
533                        resourceBundle = new PropertyResourceBundle(streamReader);
534                    }
535                }
536            }
537            return resourceBundle;
538        }
539
540    }
541
542}