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.filters;
021
022import java.io.File;
023import java.io.IOException;
024import java.nio.charset.StandardCharsets;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.List;
028import java.util.Objects;
029import java.util.Optional;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032import java.util.regex.PatternSyntaxException;
033
034import com.puppycrawl.tools.checkstyle.PropertyType;
035import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
036import com.puppycrawl.tools.checkstyle.api.AuditEvent;
037import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
038import com.puppycrawl.tools.checkstyle.api.FileText;
039import com.puppycrawl.tools.checkstyle.api.Filter;
040import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
041
042/**
043 * <p>
044 * Filter {@code SuppressWithPlainTextCommentFilter} uses plain text to suppress
045 * audit events. The filter can be used only to suppress audit events received
046 * from the checks which implement FileSetCheck interface. In other words, the
047 * checks which have Checker as a parent module. The filter knows nothing about
048 * AST, it treats only plain text comments and extracts the information required
049 * for suppression from the plain text comments. Currently, the filter supports
050 * only single-line comments.
051 * </p>
052 * <p>
053 * Please, be aware of the fact that, it is not recommended to use the filter
054 * for Java code anymore, however you still are able to use it to suppress audit
055 * events received from the checks which implement FileSetCheck interface.
056 * </p>
057 * <p>
058 * Rationale: Sometimes there are legitimate reasons for violating a check.
059 * When this is a matter of the code in question and not personal preference,
060 * the best place to override the policy is in the code itself. Semi-structured
061 * comments can be associated with the check. This is sometimes superior to
062 * a separate suppressions file, which must be kept up-to-date as the source
063 * file is edited.
064 * </p>
065 * <p>
066 * Note that the suppression comment should be put before the violation.
067 * You can use more than one suppression comment each on separate line.
068 * </p>
069 * <p>
070 * Properties {@code offCommentFormat} and {@code onCommentFormat} must have equal
071 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Matcher.html#groupCount()">
072 * paren counts</a>.
073 * </p>
074 * <p>
075 * SuppressionWithPlainTextCommentFilter can suppress Checks that have Treewalker or
076 * Checker as parent module.
077 * </p>
078 * <ul>
079 * <li>
080 * Property {@code offCommentFormat} - Specify comment pattern to trigger filter
081 * to begin suppression.
082 * Type is {@code java.util.regex.Pattern}.
083 * Default value is {@code "// CHECKSTYLE:OFF"}.
084 * </li>
085 * <li>
086 * Property {@code onCommentFormat} - Specify comment pattern to trigger filter
087 * to end suppression.
088 * Type is {@code java.util.regex.Pattern}.
089 * Default value is {@code "// CHECKSTYLE:ON"}.
090 * </li>
091 * <li>
092 * Property {@code checkFormat} - Specify check pattern to suppress.
093 * Type is {@code java.util.regex.Pattern}.
094 * Default value is {@code ".*"}.
095 * </li>
096 * <li>
097 * Property {@code messageFormat} - Specify message pattern to suppress.
098 * Type is {@code java.util.regex.Pattern}.
099 * Default value is {@code null}.
100 * </li>
101 * <li>
102 * Property {@code idFormat} - Specify check ID pattern to suppress.
103 * Type is {@code java.util.regex.Pattern}.
104 * Default value is {@code null}.
105 * </li>
106 * </ul>
107 * <p>
108 * To configure a filter to suppress audit events between a comment containing
109 * {@code CHECKSTYLE:OFF} and a comment containing {@code CHECKSTYLE:ON}:
110 * </p>
111 * <pre>
112 * &lt;module name=&quot;Checker&quot;&gt;
113 *   ...
114 *   &lt;module name=&quot;SuppressWithPlainTextCommentFilter&quot;/&gt;
115 *   ...
116 * &lt;/module&gt;
117 * </pre>
118 * <p>
119 * To configure a filter to suppress audit events between a comment containing
120 * line {@code BEGIN GENERATED CONTENT} and a comment containing line
121 * {@code END GENERATED CONTENT}(Checker is configured to check only properties files):
122 * </p>
123 * <pre>
124 * &lt;module name=&quot;Checker&quot;&gt;
125 *   &lt;property name=&quot;fileExtensions&quot; value=&quot;properties&quot;/&gt;
126 *
127 *   &lt;module name=&quot;SuppressWithPlainTextCommentFilter&quot;&gt;
128 *     &lt;property name=&quot;offCommentFormat&quot; value=&quot;BEGIN GENERATED CONTENT&quot;/&gt;
129 *     &lt;property name=&quot;onCommentFormat&quot; value=&quot;END GENERATED CONTENT&quot;/&gt;
130 *   &lt;/module&gt;
131 *
132 * &lt;/module&gt;
133 * </pre>
134 * <pre>
135 * //BEGIN GENERATED CONTENT
136 * my.property=value1 // No violation events will be reported
137 * my.property=value2 // No violation events will be reported
138 * //END GENERATED CONTENT
139 * . . .
140 * </pre>
141 * <p>
142 * To configure a filter so that {@code -- stop tab check} and {@code -- resume tab check}
143 * marks allowed tab positions (Checker is configured to check only sql files):
144 * </p>
145 * <pre>
146 * &lt;module name=&quot;Checker&quot;&gt;
147 *   &lt;property name=&quot;fileExtensions&quot; value=&quot;sql&quot;/&gt;
148 *
149 *   &lt;module name=&quot;SuppressWithPlainTextCommentFilter&quot;&gt;
150 *     &lt;property name=&quot;offCommentFormat&quot; value=&quot;stop tab check&quot;/&gt;
151 *     &lt;property name=&quot;onCommentFormat&quot; value=&quot;resume tab check&quot;/&gt;
152 *     &lt;property name=&quot;checkFormat&quot; value=&quot;FileTabCharacterCheck&quot;/&gt;
153 *   &lt;/module&gt;
154 *
155 * &lt;/module&gt;
156 * </pre>
157 * <pre>
158 * -- stop tab check
159 *   SELECT * FROM users // won't warn here if there is a tab character on line
160 * -- resume tab check
161 *   SELECT 1 // will warn here if there is a tab character on line
162 * </pre>
163 * <p>
164 * To configure a filter so that name of suppressed check mentioned in comment
165 * {@code CSOFF: <i>regexp</i>} and {@code CSON: <i>regexp</i>} mark a matching
166 * check (Checker is configured to check only xml files):
167 * </p>
168 * <pre>
169 * &lt;module name=&quot;Checker&quot;&gt;
170 *   &lt;property name=&quot;fileExtensions&quot; value=&quot;xml&quot;/&gt;
171 *
172 *   &lt;module name=&quot;SuppressWithPlainTextCommentFilter&quot;&gt;
173 *     &lt;property name=&quot;offCommentFormat&quot; value=&quot;CSOFF\: ([\w\|]+)&quot;/&gt;
174 *     &lt;property name=&quot;onCommentFormat&quot; value=&quot;CSON\: ([\w\|]+)&quot;/&gt;
175 *     &lt;property name=&quot;checkFormat&quot; value=&quot;$1&quot;/&gt;
176 *   &lt;/module&gt;
177 *
178 * &lt;/module&gt;
179 * </pre>
180 * <pre>
181 * // CSOFF: RegexpSinglelineCheck
182 *  // RegexpSingleline check won't warn any lines below here if the line matches regexp
183 * &lt;condition property=&quot;checkstyle.ant.skip&quot;&gt;
184 *   &lt;isset property=&quot;checkstyle.ant.skip&quot;/&gt;
185 * &lt;/condition&gt;
186 * // CSON: RegexpSinglelineCheck
187 * // RegexpSingleline check will warn below here if the line matches regexp
188 * &lt;property name=&quot;checkstyle.pattern.todo&quot; value=&quot;NOTHingWillMatCH_-&quot;/&gt;
189 * </pre>
190 * <p>
191 * To configure a filter to suppress all audit events between a comment containing
192 * {@code CHECKSTYLE_OFF: ALMOST_ALL} and a comment containing {@code CHECKSTYLE_OFF: ALMOST_ALL}
193 * except for the <em>EqualsHashCode</em> check (Checker is configured to check only java files):
194 * </p>
195 * <pre>
196 * &lt;module name=&quot;Checker&quot;&gt;
197 *   &lt;property name=&quot;fileExtensions&quot; value=&quot;java&quot;/&gt;
198 *
199 *   &lt;module name=&quot;SuppressWithPlainTextCommentFilter&quot;&gt;
200 *     &lt;property name=&quot;offCommentFormat&quot;
201 *       value=&quot;CHECKSTYLE_OFF: ALMOST_ALL&quot;/&gt;
202 *     &lt;property name=&quot;onCommentFormat&quot;
203 *       value=&quot;CHECKSTYLE_ON: ALMOST_ALL&quot;/&gt;
204 *     &lt;property name=&quot;checkFormat&quot;
205 *       value=&quot;^((?!(FileTabCharacterCheck)).)*$&quot;/&gt;
206 *   &lt;/module&gt;
207 *
208 * &lt;/module&gt;
209 * </pre>
210 * <pre>
211 * // CHECKSTYLE_OFF: ALMOST_ALL
212 * public static final int array [];
213 * private String [] strArray;
214 * // CHECKSTYLE_ON: ALMOST_ALL
215 * private int array1 [];
216 * </pre>
217 * <p>
218 * To configure a filter to suppress Check's violation message <b>which matches
219 * specified message in messageFormat</b>(so suppression will not be only by
220 * Check's name, but also by message text, as the same Check can report violations
221 * with different message format) between a comment containing {@code stop} and
222 * comment containing {@code resume}:
223 * </p>
224 * <pre>
225 * &lt;module name=&quot;Checker&quot;&gt;
226 *   &lt;module name=&quot;SuppressWithPlainTextCommentFilter&quot;&gt;
227 *     &lt;property name=&quot;offCommentFormat&quot; value=&quot;stop&quot;/&gt;
228 *     &lt;property name=&quot;onCommentFormat&quot; value=&quot;resume&quot;/&gt;
229 *     &lt;property name=&quot;checkFormat&quot; value=&quot;FileTabCharacterCheck&quot;/&gt;
230 *     &lt;property name=&quot;messageFormat&quot;
231 *         value=&quot;^File contains tab characters (this is the first instance)\.$&quot;/&gt;
232 *   &lt;/module&gt;
233 * &lt;/module&gt;
234 * </pre>
235 * <p>
236 * It is possible to specify an ID of checks, so that it can be leveraged by the
237 * SuppressWithPlainTextCommentFilter to skip validations. The following examples
238 * show how to skip validations near code that is surrounded with
239 * {@code -- CSOFF &lt;ID&gt; (reason)} and {@code -- CSON &lt;ID&gt;},
240 * where ID is the ID of checks you want to suppress.
241 * </p>
242 * <p>
243 * Examples of Checkstyle checks configuration:
244 * </p>
245 * <pre>
246 * &lt;module name=&quot;RegexpSinglelineJava&quot;&gt;
247 *   &lt;property name=&quot;id&quot; value=&quot;count&quot;/&gt;
248 *   &lt;property name=&quot;format&quot; value=&quot;^.*COUNT(*).*$&quot;/&gt;
249 *   &lt;property name=&quot;message&quot;
250 *     value=&quot;Don't use COUNT(*), use COUNT(1) instead.&quot;/&gt;
251 * &lt;/module&gt;
252 *
253 * &lt;module name=&quot;RegexpSinglelineJava&quot;&gt;
254 *   &lt;property name=&quot;id&quot; value=&quot;join&quot;/&gt;
255 *   &lt;property name=&quot;format&quot; value=&quot;^.*JOIN\s.+\s(ON|USING)$&quot;/&gt;
256 *   &lt;property name=&quot;message&quot;
257 *     value=&quot;Don't use JOIN, use sub-select instead.&quot;/&gt;
258 * &lt;/module&gt;
259 * </pre>
260 * <p>
261 * Example of SuppressWithPlainTextCommentFilter configuration (checkFormat which
262 * is set to '$1' points that ID of the checks is in the first group of offCommentFormat
263 * and onCommentFormat regular expressions):
264 * </p>
265 * <pre>
266 * &lt;module name="Checker"&gt;
267 *   &lt;property name="fileExtensions" value="sql"/&gt;
268 *
269 *   &lt;module name="SuppressWithPlainTextCommentFilter"&gt;
270 *     &lt;property name="offCommentFormat" value="CSOFF (\w+) \(\w+\)"/&gt;
271 *     &lt;property name="onCommentFormat" value="CSON (\w+)"/&gt;
272 *     &lt;property name="idFormat" value="$1"/&gt;
273 *   &lt;/module&gt;
274 *
275 * &lt;/module&gt;
276 * </pre>
277 * <pre>
278 * -- CSOFF join (it is ok to use join here for performance reasons)
279 * SELECT name, job_name
280 * FROM users AS u
281 * JOIN jobs AS j ON u.job_id = j.id
282 * -- CSON join
283 *
284 * -- CSOFF count (test query execution plan)
285 * EXPLAIN SELECT COUNT(*) FROM restaurants
286 * -- CSON count
287 * </pre>
288 * <p>
289 * Example of how to configure the check to suppress more than one check
290 * (Checker is configured to check only sql files).
291 * </p>
292 * <pre>
293 * &lt;module name="Checker"&gt;
294 *   &lt;property name="fileExtensions" value="sql"/&gt;
295 *
296 *   &lt;module name="SuppressWithPlainTextCommentFilter"&gt;
297 *     &lt;property name="offCommentFormat" value="@cs-\: ([\w\|]+)"/&gt;
298 *     &lt;property name="checkFormat" value="$1"/&gt;
299 *   &lt;/module&gt;
300 *
301 * &lt;/module&gt;
302 * </pre>
303 * <pre>
304 * -- @cs-: RegexpSinglelineCheck
305 * -- @cs-: FileTabCharacterCheck
306 * CREATE TABLE STATION (
307 *   ID INTEGER PRIMARY KEY,
308 *   CITY CHAR(20),
309 *   STATE CHAR(2),
310 *   LAT_N REAL,
311 *   LONG_W REAL);
312 * </pre>
313 * <p>
314 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
315 * </p>
316 *
317 * @since 8.6
318 */
319public class SuppressWithPlainTextCommentFilter extends AutomaticBean implements Filter {
320
321    /** Comment format which turns checkstyle reporting off. */
322    private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";
323
324    /** Comment format which turns checkstyle reporting on. */
325    private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";
326
327    /** Default check format to suppress. By default, the filter suppress all checks. */
328    private static final String DEFAULT_CHECK_FORMAT = ".*";
329
330    /** Specify comment pattern to trigger filter to begin suppression. */
331    private Pattern offCommentFormat = CommonUtil.createPattern(DEFAULT_OFF_FORMAT);
332
333    /** Specify comment pattern to trigger filter to end suppression. */
334    private Pattern onCommentFormat = CommonUtil.createPattern(DEFAULT_ON_FORMAT);
335
336    /** Specify check pattern to suppress. */
337    @XdocsPropertyType(PropertyType.PATTERN)
338    private String checkFormat = DEFAULT_CHECK_FORMAT;
339
340    /** Specify message pattern to suppress. */
341    @XdocsPropertyType(PropertyType.PATTERN)
342    private String messageFormat;
343
344    /** Specify check ID pattern to suppress. */
345    @XdocsPropertyType(PropertyType.PATTERN)
346    private String idFormat;
347
348    /**
349     * Setter to specify comment pattern to trigger filter to begin suppression.
350     *
351     * @param pattern off comment format pattern.
352     */
353    public final void setOffCommentFormat(Pattern pattern) {
354        offCommentFormat = pattern;
355    }
356
357    /**
358     * Setter to specify comment pattern to trigger filter to end suppression.
359     *
360     * @param pattern  on comment format pattern.
361     */
362    public final void setOnCommentFormat(Pattern pattern) {
363        onCommentFormat = pattern;
364    }
365
366    /**
367     * Setter to specify check pattern to suppress.
368     *
369     * @param format pattern for check format.
370     */
371    public final void setCheckFormat(String format) {
372        checkFormat = format;
373    }
374
375    /**
376     * Setter to specify message pattern to suppress.
377     *
378     * @param format pattern for message format.
379     */
380    public final void setMessageFormat(String format) {
381        messageFormat = format;
382    }
383
384    /**
385     * Setter to specify check ID pattern to suppress.
386     *
387     * @param format pattern for check ID format
388     */
389    public final void setIdFormat(String format) {
390        idFormat = format;
391    }
392
393    @Override
394    public boolean accept(AuditEvent event) {
395        boolean accepted = true;
396        if (event.getViolation() != null) {
397            final FileText fileText = getFileText(event.getFileName());
398            if (fileText != null) {
399                final List<Suppression> suppressions = getSuppressions(fileText);
400                accepted = getNearestSuppression(suppressions, event) == null;
401            }
402        }
403        return accepted;
404    }
405
406    @Override
407    protected void finishLocalSetup() {
408        // No code by default
409    }
410
411    /**
412     * Returns {@link FileText} instance created based on the given file name.
413     *
414     * @param fileName the name of the file.
415     * @return {@link FileText} instance.
416     * @throws IllegalStateException if the file could not be read.
417     */
418    private static FileText getFileText(String fileName) {
419        final File file = new File(fileName);
420        FileText result = null;
421
422        // some violations can be on a directory, instead of a file
423        if (!file.isDirectory()) {
424            try {
425                result = new FileText(file, StandardCharsets.UTF_8.name());
426            }
427            catch (IOException ex) {
428                throw new IllegalStateException("Cannot read source file: " + fileName, ex);
429            }
430        }
431
432        return result;
433    }
434
435    /**
436     * Returns the list of {@link Suppression} instances retrieved from the given {@link FileText}.
437     *
438     * @param fileText {@link FileText} instance.
439     * @return list of {@link Suppression} instances.
440     */
441    private List<Suppression> getSuppressions(FileText fileText) {
442        final List<Suppression> suppressions = new ArrayList<>();
443        for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
444            final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
445            suppression.ifPresent(suppressions::add);
446        }
447        return suppressions;
448    }
449
450    /**
451     * Tries to extract the suppression from the given line.
452     *
453     * @param fileText {@link FileText} instance.
454     * @param lineNo line number.
455     * @return {@link Optional} of {@link Suppression}.
456     */
457    private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
458        final String line = fileText.get(lineNo);
459        final Matcher onCommentMatcher = onCommentFormat.matcher(line);
460        final Matcher offCommentMatcher = offCommentFormat.matcher(line);
461
462        Suppression suppression = null;
463        if (onCommentMatcher.find()) {
464            suppression = new Suppression(onCommentMatcher.group(0),
465                lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this);
466        }
467        if (offCommentMatcher.find()) {
468            suppression = new Suppression(offCommentMatcher.group(0),
469                lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this);
470        }
471
472        return Optional.ofNullable(suppression);
473    }
474
475    /**
476     * Finds the nearest {@link Suppression} instance which can suppress
477     * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
478     * is before the line and column of the event.
479     *
480     * @param suppressions collection of {@link Suppression} instances.
481     * @param event {@link AuditEvent} instance.
482     * @return {@link Suppression} instance.
483     */
484    private static Suppression getNearestSuppression(Collection<Suppression> suppressions,
485                                                     AuditEvent event) {
486        return suppressions
487            .stream()
488            .filter(suppression -> suppression.isMatch(event))
489            .reduce((first, second) -> second)
490            .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
491            .orElse(null);
492    }
493
494    /** Enum which represents the type of the suppression. */
495    private enum SuppressionType {
496
497        /** On suppression type. */
498        ON,
499        /** Off suppression type. */
500        OFF,
501
502    }
503
504    /** The class which represents the suppression. */
505    private static final class Suppression {
506
507        /** The regexp which is used to match the event source.*/
508        private final Pattern eventSourceRegexp;
509        /** The regexp which is used to match the event message.*/
510        private final Pattern eventMessageRegexp;
511        /** The regexp which is used to match the event ID.*/
512        private final Pattern eventIdRegexp;
513
514        /** Suppression text.*/
515        private final String text;
516        /** Suppression line.*/
517        private final int lineNo;
518        /** Suppression column number.*/
519        private final int columnNo;
520        /** Suppression type. */
521        private final SuppressionType suppressionType;
522
523        /**
524         * Creates new suppression instance.
525         *
526         * @param text suppression text.
527         * @param lineNo suppression line number.
528         * @param columnNo suppression column number.
529         * @param suppressionType suppression type.
530         * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
531         * @throws IllegalArgumentException if there is an error in the filter regex syntax.
532         */
533        /* package */ Suppression(
534            String text,
535            int lineNo,
536            int columnNo,
537            SuppressionType suppressionType,
538            SuppressWithPlainTextCommentFilter filter
539        ) {
540            this.text = text;
541            this.lineNo = lineNo;
542            this.columnNo = columnNo;
543            this.suppressionType = suppressionType;
544
545            final Pattern commentFormat;
546            if (this.suppressionType == SuppressionType.ON) {
547                commentFormat = filter.onCommentFormat;
548            }
549            else {
550                commentFormat = filter.offCommentFormat;
551            }
552
553            // Expand regexp for check and message
554            // Does not intern Patterns with Utils.getPattern()
555            String format = "";
556            try {
557                format = CommonUtil.fillTemplateWithStringsByRegexp(
558                        filter.checkFormat, text, commentFormat);
559                eventSourceRegexp = Pattern.compile(format);
560                if (filter.messageFormat == null) {
561                    eventMessageRegexp = null;
562                }
563                else {
564                    format = CommonUtil.fillTemplateWithStringsByRegexp(
565                            filter.messageFormat, text, commentFormat);
566                    eventMessageRegexp = Pattern.compile(format);
567                }
568                if (filter.idFormat == null) {
569                    eventIdRegexp = null;
570                }
571                else {
572                    format = CommonUtil.fillTemplateWithStringsByRegexp(
573                            filter.idFormat, text, commentFormat);
574                    eventIdRegexp = Pattern.compile(format);
575                }
576            }
577            catch (final PatternSyntaxException ex) {
578                throw new IllegalArgumentException(
579                    "unable to parse expanded comment " + format, ex);
580            }
581        }
582
583        /**
584         * Indicates whether some other object is "equal to" this one.
585         * Suppression on enumeration is needed so code stays consistent.
586         *
587         * @noinspection EqualsCalledOnEnumConstant
588         */
589        @Override
590        public boolean equals(Object other) {
591            if (this == other) {
592                return true;
593            }
594            if (other == null || getClass() != other.getClass()) {
595                return false;
596            }
597            final Suppression suppression = (Suppression) other;
598            return Objects.equals(lineNo, suppression.lineNo)
599                    && Objects.equals(columnNo, suppression.columnNo)
600                    && Objects.equals(suppressionType, suppression.suppressionType)
601                    && Objects.equals(text, suppression.text)
602                    && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
603                    && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp)
604                    && Objects.equals(eventIdRegexp, suppression.eventIdRegexp);
605        }
606
607        @Override
608        public int hashCode() {
609            return Objects.hash(
610                text, lineNo, columnNo, suppressionType, eventSourceRegexp, eventMessageRegexp,
611                eventIdRegexp);
612        }
613
614        /**
615         * Checks whether the suppression matches the given {@link AuditEvent}.
616         *
617         * @param event {@link AuditEvent} instance.
618         * @return true if the suppression matches {@link AuditEvent}.
619         */
620        private boolean isMatch(AuditEvent event) {
621            return isInScopeOfSuppression(event)
622                    && isCheckMatch(event)
623                    && isIdMatch(event)
624                    && isMessageMatch(event);
625        }
626
627        /**
628         * Checks whether {@link AuditEvent} is in the scope of the suppression.
629         *
630         * @param event {@link AuditEvent} instance.
631         * @return true if {@link AuditEvent} is in the scope of the suppression.
632         */
633        private boolean isInScopeOfSuppression(AuditEvent event) {
634            return lineNo <= event.getLine();
635        }
636
637        /**
638         * Checks whether {@link AuditEvent} source name matches the check format.
639         *
640         * @param event {@link AuditEvent} instance.
641         * @return true if the {@link AuditEvent} source name matches the check format.
642         */
643        private boolean isCheckMatch(AuditEvent event) {
644            final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName());
645            return checkMatcher.find();
646        }
647
648        /**
649         * Checks whether the {@link AuditEvent} module ID matches the ID format.
650         *
651         * @param event {@link AuditEvent} instance.
652         * @return true if the {@link AuditEvent} module ID matches the ID format.
653         */
654        private boolean isIdMatch(AuditEvent event) {
655            boolean match = true;
656            if (eventIdRegexp != null) {
657                if (event.getModuleId() == null) {
658                    match = false;
659                }
660                else {
661                    final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId());
662                    match = idMatcher.find();
663                }
664            }
665            return match;
666        }
667
668        /**
669         * Checks whether the {@link AuditEvent} message matches the message format.
670         *
671         * @param event {@link AuditEvent} instance.
672         * @return true if the {@link AuditEvent} message matches the message format.
673         */
674        private boolean isMessageMatch(AuditEvent event) {
675            boolean match = true;
676            if (eventMessageRegexp != null) {
677                final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage());
678                match = messageMatcher.find();
679            }
680            return match;
681        }
682    }
683
684}