1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.checks.header;
21
22 import java.io.BufferedInputStream;
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.io.LineNumberReader;
27 import java.net.URI;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Set;
32 import java.util.regex.Pattern;
33 import java.util.regex.PatternSyntaxException;
34 import java.util.stream.Collectors;
35
36 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
37 import com.puppycrawl.tools.checkstyle.PropertyType;
38 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
39 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
40 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
41 import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
42 import com.puppycrawl.tools.checkstyle.api.FileText;
43 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 @FileStatefulCheck
86 public class MultiFileRegexpHeaderCheck
87 extends AbstractFileSetCheck implements ExternalResourceHolder {
88
89
90
91 public static final int MISMATCH_CODE = -1;
92
93
94
95
96
97 public static final String MSG_HEADER_MISSING = "multi.file.regexp.header.missing";
98
99
100
101
102
103 public static final String MSG_HEADER_MISMATCH = "multi.file.regexp.header.mismatch";
104
105
106
107
108 private static final String EMPTY_LINE_PATTERN = "^$";
109
110
111
112
113 private static final Pattern BLANK_LINE = Pattern.compile(EMPTY_LINE_PATTERN);
114
115
116
117
118
119 private final List<HeaderFileMetadata> headerFilesMetadata = new ArrayList<>();
120
121
122
123
124
125
126
127 @XdocsPropertyType(PropertyType.STRING)
128 private String headerFiles;
129
130
131
132
133
134
135
136
137
138
139
140 public void setHeaderFiles(String... headerFiles) {
141 final String[] files;
142 if (headerFiles == null) {
143 files = CommonUtil.EMPTY_STRING_ARRAY;
144 }
145 else {
146 files = headerFiles.clone();
147 }
148
149 headerFilesMetadata.clear();
150
151 for (final String headerFile : files) {
152 headerFilesMetadata.add(HeaderFileMetadata.createFromFile(headerFile));
153 }
154 }
155
156
157
158
159
160
161
162 public String getConfiguredHeaderPaths() {
163 return headerFilesMetadata.stream()
164 .map(HeaderFileMetadata::getHeaderFilePath)
165 .collect(Collectors.joining(", "));
166 }
167
168 @Override
169 public Set<String> getExternalResourceLocations() {
170 return headerFilesMetadata.stream()
171 .map(HeaderFileMetadata::getHeaderFileUri)
172 .map(URI::toASCIIString)
173 .collect(Collectors.toUnmodifiableSet());
174 }
175
176 @Override
177 protected void processFiltered(File file, FileText fileText) {
178 if (!headerFilesMetadata.isEmpty()) {
179 final List<MatchResult> matchResult = headerFilesMetadata.stream()
180 .map(headerFile -> matchHeader(fileText, headerFile))
181 .collect(Collectors.toUnmodifiableList());
182
183 if (matchResult.stream().noneMatch(match -> match.isMatching)) {
184 final MatchResult mismatch = matchResult.get(0);
185 final String allConfiguredHeaderPaths = getConfiguredHeaderPaths();
186 log(mismatch.lineNumber, mismatch.messageKey,
187 mismatch.messageArg, allConfiguredHeaderPaths);
188 }
189 }
190 }
191
192
193
194
195
196
197
198
199 private static MatchResult matchHeader(FileText fileText, HeaderFileMetadata headerFile) {
200 final int fileSize = fileText.size();
201 final List<Pattern> headerPatterns = headerFile.getHeaderPatterns();
202 final int headerPatternSize = headerPatterns.size();
203
204 int mismatchLine = MISMATCH_CODE;
205 int index;
206 for (index = 0; index < headerPatternSize && index < fileSize; index++) {
207 if (!headerPatterns.get(index).matcher(fileText.get(index)).find()) {
208 mismatchLine = index;
209 break;
210 }
211 }
212 if (index < headerPatternSize) {
213 mismatchLine = index;
214 }
215
216 final MatchResult matchResult;
217 if (mismatchLine == MISMATCH_CODE) {
218 matchResult = MatchResult.matching();
219 }
220 else {
221 matchResult = createMismatchResult(headerFile, fileText, mismatchLine);
222 }
223 return matchResult;
224 }
225
226
227
228
229
230
231
232
233
234 private static MatchResult createMismatchResult(HeaderFileMetadata headerFile,
235 FileText fileText, int mismatchLine) {
236 final String messageKey;
237 final int lineToLog;
238 final String messageArg;
239
240 if (headerFile.getHeaderPatterns().size() > fileText.size()) {
241 messageKey = MSG_HEADER_MISSING;
242 lineToLog = 1;
243 messageArg = headerFile.getHeaderFilePath();
244 }
245 else {
246 messageKey = MSG_HEADER_MISMATCH;
247 lineToLog = mismatchLine + 1;
248 final String lineContent = headerFile.getLineContents().get(mismatchLine);
249 if (lineContent.isEmpty()) {
250 messageArg = EMPTY_LINE_PATTERN;
251 }
252 else {
253 messageArg = lineContent;
254 }
255 }
256 return MatchResult.mismatch(lineToLog, messageKey, messageArg);
257 }
258
259
260
261
262
263
264
265
266
267 public static List<String> getLines(String headerFile, URI uri) {
268 final List<String> readerLines = new ArrayList<>();
269 try (LineNumberReader lineReader = new LineNumberReader(
270 new InputStreamReader(
271 new BufferedInputStream(uri.toURL().openStream()),
272 StandardCharsets.UTF_8)
273 )) {
274 String line;
275 do {
276 line = lineReader.readLine();
277 if (line != null) {
278 readerLines.add(line);
279 }
280 } while (line != null);
281 }
282 catch (final IOException exc) {
283 throw new IllegalArgumentException("unable to load header file " + headerFile, exc);
284 }
285
286 if (readerLines.isEmpty()) {
287 throw new IllegalArgumentException("Header file is empty: " + headerFile);
288 }
289 return readerLines;
290 }
291
292
293
294
295 private static final class HeaderFileMetadata {
296
297 private final URI headerFileUri;
298
299 private final String headerFilePath;
300
301 private final List<Pattern> headerPatterns;
302
303 private final List<String> lineContents;
304
305
306
307
308
309
310
311
312
313 private HeaderFileMetadata(
314 URI headerFileUri, String headerFilePath,
315 List<Pattern> headerPatterns, List<String> lineContents
316 ) {
317 this.headerFileUri = headerFileUri;
318 this.headerFilePath = headerFilePath;
319 this.headerPatterns = headerPatterns;
320 this.lineContents = lineContents;
321 }
322
323
324
325
326
327
328
329
330
331 public static HeaderFileMetadata createFromFile(String headerPath) {
332 if (CommonUtil.isBlank(headerPath)) {
333 throw new IllegalArgumentException("Header file is not set");
334 }
335 try {
336 final URI uri = CommonUtil.getUriByFilename(headerPath);
337 final List<String> readerLines = getLines(headerPath, uri);
338 final List<Pattern> patterns = readerLines.stream()
339 .map(HeaderFileMetadata::createPatternFromLine)
340 .collect(Collectors.toUnmodifiableList());
341 return new HeaderFileMetadata(uri, headerPath, patterns, readerLines);
342 }
343 catch (CheckstyleException exc) {
344 throw new IllegalArgumentException(
345 "Error reading or corrupted header file: " + headerPath, exc);
346 }
347 }
348
349
350
351
352
353
354
355 private static Pattern createPatternFromLine(String line) {
356 final Pattern result;
357 if (line.isEmpty()) {
358 result = BLANK_LINE;
359 }
360 else {
361 result = Pattern.compile(validateRegex(line));
362 }
363 return result;
364 }
365
366
367
368
369
370
371 public URI getHeaderFileUri() {
372 return headerFileUri;
373 }
374
375
376
377
378
379
380 public String getHeaderFilePath() {
381 return headerFilePath;
382 }
383
384
385
386
387
388
389 public List<Pattern> getHeaderPatterns() {
390 return List.copyOf(headerPatterns);
391 }
392
393
394
395
396
397
398 public List<String> getLineContents() {
399 return List.copyOf(lineContents);
400 }
401
402
403
404
405
406
407
408
409
410
411
412 private static String validateRegex(String input) {
413 try {
414 Pattern.compile(input);
415 return input;
416 }
417 catch (final PatternSyntaxException exc) {
418 throw new IllegalArgumentException("Invalid regex pattern: " + input, exc);
419 }
420 }
421 }
422
423
424
425
426 private static final class MatchResult {
427
428 private final boolean isMatching;
429
430 private final int lineNumber;
431
432 private final String messageKey;
433
434 private final String messageArg;
435
436
437
438
439
440
441
442
443
444 private MatchResult(boolean isMatching, int lineNumber, String messageKey,
445 String messageArg) {
446 this.isMatching = isMatching;
447 this.lineNumber = lineNumber;
448 this.messageKey = messageKey;
449 this.messageArg = messageArg;
450 }
451
452
453
454
455
456
457 public static MatchResult matching() {
458 return new MatchResult(true, 0, null, null);
459 }
460
461
462
463
464
465
466
467
468
469 public static MatchResult mismatch(int lineNumber, String messageKey,
470 String messageArg) {
471 return new MatchResult(false, lineNumber, messageKey, messageArg);
472 }
473 }
474 }