001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.checks.header;
021
022import java.io.File;
023import java.util.ArrayList;
024import java.util.BitSet;
025import java.util.List;
026import java.util.regex.Pattern;
027import java.util.regex.PatternSyntaxException;
028
029import com.puppycrawl.tools.checkstyle.StatelessCheck;
030import com.puppycrawl.tools.checkstyle.api.FileText;
031import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
032import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
033
034/**
035 * <div>
036 * Checks the header of a source file against a header that contains a
037 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Pattern.html">
038 * pattern</a> for each line of the source header.
039 * </div>
040 *
041 * @since 3.2
042 */
043@StatelessCheck
044public class RegexpHeaderCheck extends AbstractHeaderCheck {
045
046    /**
047     * A key is pointing to the warning message text in "messages.properties"
048     * file.
049     */
050    public static final String MSG_HEADER_MISSING = "header.missing";
051
052    /**
053     * A key is pointing to the warning message text in "messages.properties"
054     * file.
055     */
056    public static final String MSG_HEADER_MISMATCH = "header.mismatch";
057
058    /** Regex pattern for a blank line. **/
059    private static final String EMPTY_LINE_PATTERN = "^$";
060
061    /** Compiled regex pattern for a blank line. **/
062    private static final Pattern BLANK_LINE = Pattern.compile(EMPTY_LINE_PATTERN);
063
064    /** The compiled regular expressions. */
065    private final List<Pattern> headerRegexps = new ArrayList<>();
066
067    /** Specify the line numbers to repeat (zero or more times). */
068    private BitSet multiLines = new BitSet();
069
070    /**
071     * Setter to specify the line numbers to repeat (zero or more times).
072     *
073     * @param list line numbers to repeat in header.
074     * @since 3.4
075     */
076    public void setMultiLines(int... list) {
077        multiLines = TokenUtil.asBitSet(list);
078    }
079
080    @Override
081    protected void processFiltered(File file, FileText fileText) {
082        final int headerSize = getHeaderLines().size();
083        final int fileSize = fileText.size();
084
085        if (headerSize - multiLines.cardinality() > fileSize) {
086            log(1, MSG_HEADER_MISSING);
087        }
088        else {
089            int headerLineNo = 0;
090            int index;
091            for (index = 0; headerLineNo < headerSize && index < fileSize; index++) {
092                final String line = fileText.get(index);
093                boolean isMatch = isMatch(line, headerLineNo);
094                while (!isMatch && isMultiLine(headerLineNo)) {
095                    headerLineNo++;
096                    isMatch = headerLineNo == headerSize
097                            || isMatch(line, headerLineNo);
098                }
099                if (!isMatch) {
100                    log(index + 1, MSG_HEADER_MISMATCH, getHeaderLine(headerLineNo));
101                    break;
102                }
103                if (!isMultiLine(headerLineNo)) {
104                    headerLineNo++;
105                }
106            }
107            if (index == fileSize) {
108                // if file finished, but we have at least one non-multi-line
109                // header isn't completed
110                logFirstSinglelineLine(headerLineNo, headerSize);
111            }
112        }
113    }
114
115    /**
116     * Returns the line from the header. Where the line is blank return the regexp pattern
117     * for a blank line.
118     *
119     * @param headerLineNo header line number to return
120     * @return the line from the header
121     */
122    private String getHeaderLine(int headerLineNo) {
123        String line = getHeaderLines().get(headerLineNo);
124        if (line.isEmpty()) {
125            line = EMPTY_LINE_PATTERN;
126        }
127        return line;
128    }
129
130    /**
131     * Logs warning if any non-multiline lines left in header regexp.
132     *
133     * @param startHeaderLine header line number to start from
134     * @param headerSize whole header size
135     */
136    private void logFirstSinglelineLine(int startHeaderLine, int headerSize) {
137        for (int lineNum = startHeaderLine; lineNum < headerSize; lineNum++) {
138            if (!isMultiLine(lineNum)) {
139                log(1, MSG_HEADER_MISSING);
140                break;
141            }
142        }
143    }
144
145    /**
146     * Checks if a code line matches the required header line.
147     *
148     * @param line the code line
149     * @param headerLineNo the header line number.
150     * @return true if and only if the line matches the required header line.
151     */
152    private boolean isMatch(String line, int headerLineNo) {
153        return headerRegexps.get(headerLineNo).matcher(line).find();
154    }
155
156    /**
157     * Returns true if line is multiline header lines or false.
158     *
159     * @param lineNo a line number
160     * @return if {@code lineNo} is one of the repeat header lines.
161     */
162    private boolean isMultiLine(int lineNo) {
163        return multiLines.get(lineNo + 1);
164    }
165
166    @Override
167    protected void postProcessHeaderLines() {
168        final List<String> headerLines = getHeaderLines();
169        for (String line : headerLines) {
170            try {
171                if (line.isEmpty()) {
172                    headerRegexps.add(BLANK_LINE);
173                }
174                else {
175                    headerRegexps.add(Pattern.compile(line));
176                }
177            }
178            catch (final PatternSyntaxException exc) {
179                throw new IllegalArgumentException("line "
180                        + (headerRegexps.size() + 1)
181                        + " in header specification"
182                        + " is not a regular expression", exc);
183            }
184        }
185    }
186
187    /**
188     * Setter to define the required header specified inline.
189     * Individual header lines must be separated by the string {@code "\n"}
190     * (even on platforms with a different line separator).
191     * For header lines containing {@code "\n\n"} checkstyle will forcefully
192     * expect an empty line to exist. See examples below.
193     * Regular expressions must not span multiple lines.
194     *
195     * @param header the header value to validate and set (in that order)
196     * @since 5.0
197     */
198    @Override
199    public void setHeader(String header) {
200        if (!CommonUtil.isBlank(header)) {
201            if (!CommonUtil.isPatternValid(header)) {
202                throw new IllegalArgumentException("Unable to parse format: " + header);
203            }
204            super.setHeader(header);
205        }
206    }
207
208}