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