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