aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/puppycrawl/tools/checkstyle/XMLLogger.java
blob: 0a009541850d916df6f999a7351d2e4891434c2b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;

import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;

/**
 * Simple XML logger.
 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
 * we want to localize error messages or simply that file names are
 * localized and takes care about escaping as well.

 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
 */
// -@cs[AbbreviationAsWordInName] We can not change it as,
// check's name is part of API (used in configurations).
public class XMLLogger
    extends AutomaticBean
    implements AuditListener {
    /** Decimal radix. */
    private static final int BASE_10 = 10;

    /** Hex radix. */
    private static final int BASE_16 = 16;

    /** Some known entities to detect. */
    private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
                                              "quot", };

    /** Close output stream in auditFinished. */
    private final boolean closeStream;

    /** The writer lock object. */
    private final Object writerLock = new Object();

    /** Holds all messages for the given file. */
    private final Map<String, FileMessages> fileMessages =
            new ConcurrentHashMap<>();

    /**
     * Helper writer that allows easy encoding and printing.
     */
    private final PrintWriter writer;

    /**
     * Creates a new {@code XMLLogger} instance.
     * Sets the output to a defined stream.
     * @param outputStream the stream to write logs to.
     * @param closeStream close oS in auditFinished
     * @deprecated in order to fullfil demands of BooleanParameter IDEA check.
     * @noinspection BooleanParameter
     */
    @Deprecated
    public XMLLogger(OutputStream outputStream, boolean closeStream) {
        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        this.closeStream = closeStream;
    }

    /**
     * Creates a new {@code XMLLogger} instance.
     * Sets the output to a defined stream.
     * @param outputStream the stream to write logs to.
     * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
     */
    public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
    }

    @Override
    public void auditStarted(AuditEvent event) {
        writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

        final ResourceBundle compilationProperties =
            ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT);
        final String version =
            compilationProperties.getString("checkstyle.compile.version");

        writer.println("<checkstyle version=\"" + version + "\">");
    }

    @Override
    public void auditFinished(AuditEvent event) {
        fileMessages.forEach(this::writeFileMessages);

        writer.println("</checkstyle>");
        if (closeStream) {
            writer.close();
        }
        else {
            writer.flush();
        }
    }

    @Override
    public void fileStarted(AuditEvent event) {
        fileMessages.put(event.getFileName(), new FileMessages());
    }

    @Override
    public void fileFinished(AuditEvent event) {
        final String fileName = event.getFileName();
        final FileMessages messages = fileMessages.get(fileName);

        synchronized (writerLock) {
            writeFileMessages(fileName, messages);
        }

        fileMessages.remove(fileName);
    }

    /**
     * Prints the file section with all file errors and exceptions.
     * @param fileName The file name, as should be printed in the opening file tag.
     * @param messages The file messages.
     */
    private void writeFileMessages(String fileName, FileMessages messages) {
        writeFileOpeningTag(fileName);
        if (messages != null) {
            for (AuditEvent errorEvent : messages.getErrors()) {
                writeFileError(errorEvent);
            }
            for (Throwable exception : messages.getExceptions()) {
                writeException(exception);
            }
        }
        writeFileClosingTag();
    }

    /**
     * Prints the "file" opening tag with the given filename.
     * @param fileName The filename to output.
     */
    private void writeFileOpeningTag(String fileName) {
        writer.println("<file name=\"" + encode(fileName) + "\">");
    }

    /**
     * Prints the "file" closing tag.
     */
    private void writeFileClosingTag() {
        writer.println("</file>");
    }

    @Override
    public void addError(AuditEvent event) {
        if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
            final String fileName = event.getFileName();
            if (fileName == null) {
                synchronized (writerLock) {
                    writeFileError(event);
                }
            }
            else {
                final FileMessages messages = fileMessages.computeIfAbsent(
                        fileName, name -> new FileMessages());
                messages.addError(event);
            }
        }
    }

    /**
     * Outputs the given envet to the writer.
     * @param event An event to print.
     */
    private void writeFileError(AuditEvent event) {
        writer.print("<error" + " line=\"" + event.getLine() + "\"");
        if (event.getColumn() > 0) {
            writer.print(" column=\"" + event.getColumn() + "\"");
        }
        writer.print(" severity=\""
                + event.getSeverityLevel().getName()
                + "\"");
        writer.print(" message=\""
                + encode(event.getMessage())
                + "\"");
        writer.print(" source=\"");
        if (event.getModuleId() == null) {
            writer.print(encode(event.getSourceName()));
        }
        else {
            writer.print(encode(event.getModuleId()));
        }
        writer.println("\"/>");
    }

    @Override
    public void addException(AuditEvent event, Throwable throwable) {
        final String fileName = event.getFileName();
        if (fileName == null) {
            synchronized (writerLock) {
                writeException(throwable);
            }
        }
        else {
            final FileMessages messages = fileMessages.computeIfAbsent(
                    fileName, name -> new FileMessages());
            messages.addException(throwable);
        }
    }

    /**
     * Writes the exception event to the print writer.
     * @param throwable The
     */
    private void writeException(Throwable throwable) {
        final StringWriter stringWriter = new StringWriter();
        final PrintWriter printer = new PrintWriter(stringWriter);
        printer.println("<exception>");
        printer.println("<![CDATA[");
        throwable.printStackTrace(printer);
        printer.println("]]>");
        printer.println("</exception>");
        writer.println(encode(stringWriter.toString()));
    }

    /**
     * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
     * @param value the value to escape.
     * @return the escaped value if necessary.
     */
    public static String encode(String value) {
        final StringBuilder sb = new StringBuilder(256);
        for (int i = 0; i < value.length(); i++) {
            final char chr = value.charAt(i);
            switch (chr) {
                case '<':
                    sb.append("&lt;");
                    break;
                case '>':
                    sb.append("&gt;");
                    break;
                case '\'':
                    sb.append("&apos;");
                    break;
                case '\"':
                    sb.append("&quot;");
                    break;
                case '&':
                    sb.append("&amp;");
                    break;
                case '\r':
                    break;
                case '\n':
                    sb.append("&#10;");
                    break;
                default:
                    if (Character.isISOControl(chr)) {
                        // true escape characters need '&' before but it also requires XML 1.1
                        // until https://github.com/checkstyle/checkstyle/issues/5168
                        sb.append("#x");
                        sb.append(Integer.toHexString(chr));
                        sb.append(';');
                    }
                    else {
                        sb.append(chr);
                    }
                    break;
            }
        }
        return sb.toString();
    }

    /**
     * Finds whether the given argument is character or entity reference.
     * @param ent the possible entity to look for.
     * @return whether the given argument a character or entity reference
     */
    public static boolean isReference(String ent) {
        boolean reference = false;

        if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) {
            reference = false;
        }
        else if (ent.charAt(1) == '#') {
            // prefix is "&#"
            int prefixLength = 2;

            int radix = BASE_10;
            if (ent.charAt(2) == 'x') {
                prefixLength++;
                radix = BASE_16;
            }
            try {
                Integer.parseInt(
                    ent.substring(prefixLength, ent.length() - 1), radix);
                reference = true;
            }
            catch (final NumberFormatException ignored) {
                reference = false;
            }
        }
        else {
            final String name = ent.substring(1, ent.length() - 1);
            for (String element : ENTITIES) {
                if (name.equals(element)) {
                    reference = true;
                    break;
                }
            }
        }
        return reference;
    }

    /**
     * The registered file messages.
     */
    private static class FileMessages {
        /** The file error events. */
        private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());

        /** The file exceptions. */
        private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());

        /**
         * Returns the file error events.
         * @return the file error events.
         */
        public List<AuditEvent> getErrors() {
            return Collections.unmodifiableList(errors);
        }

        /**
         * Adds the given error event to the messages.
         * @param event the error event.
         */
        public void addError(AuditEvent event) {
            errors.add(event);
        }

        /**
         * Returns the file exceptions.
         * @return the file exceptions.
         */
        public List<Throwable> getExceptions() {
            return Collections.unmodifiableList(exceptions);
        }

        /**
         * Adds the given exception to the messages.
         * @param throwable the file exception
         */
        public void addException(Throwable throwable) {
            exceptions.add(throwable);
        }
    }
}