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