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