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;
21
22 import java.io.OutputStream;
23 import java.io.OutputStreamWriter;
24 import java.io.PrintWriter;
25 import java.io.StringWriter;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32
33 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
34 import com.puppycrawl.tools.checkstyle.api.AuditListener;
35 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
36 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
37
38
39
40
41
42
43
44
45
46 public final class XMLLogger
47 extends AbstractAutomaticBean
48 implements AuditListener {
49
50
51 private static final int BASE_10 = 10;
52
53
54 private static final int BASE_16 = 16;
55
56
57 private static final int SOURCE_BUILDER_CAPACITY = 128;
58
59
60 private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
61 "quot", };
62
63
64 private final boolean closeStream;
65
66
67 private final Map<String, FileMessages> fileMessages =
68 new HashMap<>();
69
70
71
72
73 private final PrintWriter writer;
74
75
76
77
78
79
80
81
82
83
84
85
86 public XMLLogger(OutputStream outputStream,
87 AutomaticBean.OutputStreamOptions outputStreamOptions) {
88 this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
89 }
90
91
92
93
94
95
96
97
98
99 public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
100 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
101 if (outputStreamOptions == null) {
102 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
103 }
104 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
105 }
106
107 @Override
108 protected void finishLocalSetup() {
109
110 }
111
112
113
114
115
116 private void printVersionString() {
117 final String version = XMLLogger.class.getPackage().getImplementationVersion();
118 writer.println("<checkstyle version=\"" + version + "\">");
119 }
120
121 @Override
122 public void auditStarted(AuditEvent event) {
123 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
124
125 printVersionString();
126 }
127
128 @Override
129 public void auditFinished(AuditEvent event) {
130 writer.println("</checkstyle>");
131 if (closeStream) {
132 writer.close();
133 }
134 else {
135 writer.flush();
136 }
137 }
138
139 @Override
140 public void fileStarted(AuditEvent event) {
141 fileMessages.put(event.getFileName(), new FileMessages());
142 }
143
144 @Override
145 public void fileFinished(AuditEvent event) {
146 final String fileName = event.getFileName();
147 final FileMessages messages = fileMessages.remove(fileName);
148 writeFileMessages(fileName, messages);
149 }
150
151
152
153
154
155
156
157 private void writeFileMessages(String fileName, FileMessages messages) {
158 writeFileOpeningTag(fileName);
159 if (messages != null) {
160 for (AuditEvent errorEvent : messages.getErrors()) {
161 writeFileError(errorEvent);
162 }
163 for (Throwable exception : messages.getExceptions()) {
164 writeException(exception);
165 }
166 }
167 writeFileClosingTag();
168 }
169
170
171
172
173
174
175 private void writeFileOpeningTag(String fileName) {
176 writer.println("<file name=\"" + encode(fileName) + "\">");
177 }
178
179
180
181
182 private void writeFileClosingTag() {
183 writer.println("</file>");
184 }
185
186 @Override
187 public void addError(AuditEvent event) {
188 if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
189 final String fileName = event.getFileName();
190 final FileMessages messages = fileMessages.get(fileName);
191 if (messages != null) {
192 messages.addError(event);
193 }
194 else {
195 writeFileError(event);
196 }
197 }
198 }
199
200
201
202
203
204
205 private void writeFileError(AuditEvent event) {
206 writer.print("<error" + " line=\"" + event.getLine() + "\"");
207 if (event.getColumn() > 0) {
208 writer.print(" column=\"" + event.getColumn() + "\"");
209 }
210 writer.print(" severity=\""
211 + event.getSeverityLevel().getName()
212 + "\"");
213 writer.print(" message=\""
214 + encode(event.getMessage())
215 + "\"");
216 writer.print(" source=\"");
217 final StringBuilder sourceValueBuilder = new StringBuilder(SOURCE_BUILDER_CAPACITY);
218 sourceValueBuilder.append(event.getSourceName());
219 final String moduleId = event.getModuleId();
220 if (moduleId != null && !moduleId.isBlank()) {
221 sourceValueBuilder
222 .append('#')
223 .append(moduleId);
224 }
225 writer.print(encode(sourceValueBuilder.toString()));
226 writer.println("\"/>");
227 }
228
229 @Override
230 public void addException(AuditEvent event, Throwable throwable) {
231 final String fileName = event.getFileName();
232 final FileMessages messages = fileMessages.get(fileName);
233 if (messages != null) {
234 messages.addException(throwable);
235 }
236 else {
237 writeException(throwable);
238 }
239 }
240
241
242
243
244
245
246 private void writeException(Throwable throwable) {
247 writer.println("<exception>");
248 writer.println("<![CDATA[");
249
250 final StringWriter stringWriter = new StringWriter();
251 final PrintWriter printer = new PrintWriter(stringWriter);
252 throwable.printStackTrace(printer);
253 writer.println(encode(stringWriter.toString()));
254
255 writer.println("]]>");
256 writer.println("</exception>");
257 }
258
259
260
261
262
263
264
265 public static String encode(String value) {
266 final StringBuilder sb = new StringBuilder(256);
267 for (int i = 0; i < value.length(); i++) {
268 final char chr = value.charAt(i);
269 switch (chr) {
270 case '<' -> sb.append("<");
271 case '>' -> sb.append(">");
272 case '\'' -> sb.append("'");
273 case '\"' -> sb.append(""");
274 case '&' -> sb.append("&");
275 case '\r' -> {
276
277 }
278 case '\n' -> sb.append(" ");
279 default -> {
280 if (Character.isISOControl(chr)) {
281
282
283 sb.append("#x");
284 sb.append(Integer.toHexString(chr));
285 sb.append(';');
286 }
287 else {
288 sb.append(chr);
289 }
290 }
291 }
292 }
293 return sb.toString();
294 }
295
296
297
298
299
300
301
302 public static boolean isReference(String ent) {
303 boolean reference = false;
304
305 if (ent.charAt(0) == '&' && ent.endsWith(";")) {
306 if (ent.charAt(1) == '#') {
307
308 int prefixLength = 2;
309
310 int radix = BASE_10;
311 if (ent.charAt(2) == 'x') {
312 prefixLength++;
313 radix = BASE_16;
314 }
315 try {
316 Integer.parseInt(
317 ent.substring(prefixLength, ent.length() - 1), radix);
318 reference = true;
319 }
320 catch (final NumberFormatException ignored) {
321 reference = false;
322 }
323 }
324 else {
325 final String name = ent.substring(1, ent.length() - 1);
326 for (String element : ENTITIES) {
327 if (name.equals(element)) {
328 reference = true;
329 break;
330 }
331 }
332 }
333 }
334
335 return reference;
336 }
337
338
339
340
341 private static final class FileMessages {
342
343
344 private final List<AuditEvent> errors = new ArrayList<>();
345
346
347 private final List<Throwable> exceptions = new ArrayList<>();
348
349
350
351
352
353
354 List<AuditEvent> getErrors() {
355 return Collections.unmodifiableList(errors);
356 }
357
358
359
360
361
362
363 void addError(AuditEvent event) {
364 errors.add(event);
365 }
366
367
368
369
370
371
372 List<Throwable> getExceptions() {
373 return Collections.unmodifiableList(exceptions);
374 }
375
376
377
378
379
380
381 void addException(Throwable throwable) {
382 exceptions.add(throwable);
383 }
384
385 }
386
387 }