001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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; 021 022import java.io.OutputStream; 023import java.io.OutputStreamWriter; 024import java.io.PrintWriter; 025import java.io.StringWriter; 026import java.nio.charset.StandardCharsets; 027import java.util.Locale; 028import java.util.ResourceBundle; 029 030import com.puppycrawl.tools.checkstyle.api.AuditEvent; 031import com.puppycrawl.tools.checkstyle.api.AuditListener; 032import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 033import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 034import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 035 036/** 037 * Simple XML logger. 038 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case 039 * we want to localize error messages or simply that file names are 040 * localized and takes care about escaping as well. 041 042 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a> 043 */ 044// -@cs[AbbreviationAsWordInName] We can not change it as, 045// check's name is part of API (used in configurations). 046public class XMLLogger 047 extends AutomaticBean 048 implements AuditListener { 049 /** Decimal radix. */ 050 private static final int BASE_10 = 10; 051 052 /** Hex radix. */ 053 private static final int BASE_16 = 16; 054 055 /** Some known entities to detect. */ 056 private static final String[] ENTITIES = {"gt", "amp", "lt", "apos", 057 "quot", }; 058 059 /** Close output stream in auditFinished. */ 060 private final boolean closeStream; 061 062 /** Helper writer that allows easy encoding and printing. */ 063 private PrintWriter writer; 064 065 /** 066 * Creates a new {@code XMLLogger} instance. 067 * Sets the output to a defined stream. 068 * @param outputStream the stream to write logs to. 069 * @param closeStream close oS in auditFinished 070 */ 071 public XMLLogger(OutputStream outputStream, boolean closeStream) { 072 setOutputStream(outputStream); 073 this.closeStream = closeStream; 074 } 075 076 /** 077 * Sets the OutputStream. 078 * @param outputStream the OutputStream to use 079 **/ 080 private void setOutputStream(OutputStream outputStream) { 081 final OutputStreamWriter osw = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); 082 writer = new PrintWriter(osw); 083 } 084 085 @Override 086 public void auditStarted(AuditEvent event) { 087 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 088 089 final ResourceBundle compilationProperties = 090 ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT); 091 final String version = 092 compilationProperties.getString("checkstyle.compile.version"); 093 094 writer.println("<checkstyle version=\"" + version + "\">"); 095 } 096 097 @Override 098 public void auditFinished(AuditEvent event) { 099 writer.println("</checkstyle>"); 100 if (closeStream) { 101 writer.close(); 102 } 103 else { 104 writer.flush(); 105 } 106 } 107 108 @Override 109 public void fileStarted(AuditEvent event) { 110 writer.println("<file name=\"" + encode(event.getFileName()) + "\">"); 111 } 112 113 @Override 114 public void fileFinished(AuditEvent event) { 115 writer.println("</file>"); 116 } 117 118 @Override 119 public void addError(AuditEvent event) { 120 if (event.getSeverityLevel() != SeverityLevel.IGNORE) { 121 writer.print("<error" + " line=\"" + event.getLine() + "\""); 122 if (event.getColumn() > 0) { 123 writer.print(" column=\"" + event.getColumn() + "\""); 124 } 125 writer.print(" severity=\"" 126 + event.getSeverityLevel().getName() 127 + "\""); 128 writer.print(" message=\"" 129 + encode(event.getMessage()) 130 + "\""); 131 writer.println(" source=\"" 132 + encode(event.getSourceName()) 133 + "\"/>"); 134 } 135 } 136 137 @Override 138 public void addException(AuditEvent event, Throwable throwable) { 139 final StringWriter stringWriter = new StringWriter(); 140 final PrintWriter printer = new PrintWriter(stringWriter); 141 printer.println("<exception>"); 142 printer.println("<![CDATA["); 143 throwable.printStackTrace(printer); 144 printer.println("]]>"); 145 printer.println("</exception>"); 146 printer.flush(); 147 writer.println(encode(stringWriter.toString())); 148 } 149 150 /** 151 * Escape <, > & ' and " as their entities. 152 * @param value the value to escape. 153 * @return the escaped value if necessary. 154 */ 155 public static String encode(String value) { 156 final StringBuilder sb = new StringBuilder(); 157 for (int i = 0; i < value.length(); i++) { 158 final char chr = value.charAt(i); 159 switch (chr) { 160 case '<': 161 sb.append("<"); 162 break; 163 case '>': 164 sb.append(">"); 165 break; 166 case '\'': 167 sb.append("'"); 168 break; 169 case '\"': 170 sb.append("""); 171 break; 172 case '&': 173 sb.append(encodeAmpersand(value, i)); 174 break; 175 case '\r': 176 break; 177 case '\n': 178 sb.append(" "); 179 break; 180 default: 181 sb.append(chr); 182 break; 183 } 184 } 185 return sb.toString(); 186 } 187 188 /** 189 * Finds whether the given argument is character or entity reference. 190 * @param ent the possible entity to look for. 191 * @return whether the given argument a character or entity reference 192 */ 193 public static boolean isReference(String ent) { 194 boolean reference = false; 195 196 if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) { 197 reference = false; 198 } 199 else if (ent.charAt(1) == '#') { 200 // prefix is "&#" 201 int prefixLength = 2; 202 203 int radix = BASE_10; 204 if (ent.charAt(2) == 'x') { 205 prefixLength++; 206 radix = BASE_16; 207 } 208 try { 209 Integer.parseInt( 210 ent.substring(prefixLength, ent.length() - 1), radix); 211 reference = true; 212 } 213 catch (final NumberFormatException ignored) { 214 reference = false; 215 } 216 } 217 else { 218 final String name = ent.substring(1, ent.length() - 1); 219 for (String element : ENTITIES) { 220 if (name.equals(element)) { 221 reference = true; 222 break; 223 } 224 } 225 } 226 return reference; 227 } 228 229 /** 230 * Encodes ampersand in value at required position. 231 * @param value string value, which contains ampersand 232 * @param ampPosition position of ampersand in value 233 * @return encoded ampersand which should be used in xml 234 */ 235 private static String encodeAmpersand(String value, int ampPosition) { 236 final int nextSemi = value.indexOf(';', ampPosition); 237 final String result; 238 if (nextSemi < 0 239 || !isReference(value.substring(ampPosition, nextSemi + 1))) { 240 result = "&"; 241 } 242 else { 243 result = "&"; 244 } 245 return result; 246 } 247}