/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.renderers; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.util.Iterator; import java.util.Map; import org.apache.commons.io.IOUtils; import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.Report; import net.sourceforge.pmd.RuleViolation; import net.sourceforge.pmd.lang.rule.properties.StringProperty; /** * <p> * A console renderer with optional color support under *nix systems. * </p> * * <pre> * * file: ./src/gilot/Test.java * src: Test.java:12 * rule: AtLeastOneConstructor * msg: Each class should declare at least one constructor * code: public class Test * * * file: ./src/gilot/log/format/LogInterpreter.java * src: LogInterpreter.java:317 * rule: AvoidDuplicateLiterals * msg: The same String literal appears 4 times in this file; the first occurrence is on line 317 * code: logger.error( "missing attribute 'app_arg' in rule '" + ((Element)element.getParent()).getAttributeValue( "name" ) + "'" ); * * src: LogInterpreter.java:317 * rule: AvoidDuplicateLiterals * msg: The same String literal appears 5 times in this file; the first occurrence is on line 317 * code: logger.error( "missing attribute 'app_arg' in rule '" + ((Element)element.getParent()).getAttributeValue( "name" ) + "'" ); * * warnings: 3 * </pre> * <p> * Colorization is turned on by supplying -D<b>pmd.color</b> - any value other * than '0' or 'false', enables color - including an empty value (''). <b>Nota * Bene:</b> colorization is atm only supported under *nix terminals accepting * ansi escape sequences, such as xterm, rxvt et cetera. * </p> */ public class TextColorRenderer extends AbstractAccumulatingRenderer { public static final String NAME = "textcolor"; public static final StringProperty COLOR = new StringProperty("color", "Enables colors with anything other than 'false' or '0'.", "yes", 0); private static final String SYSTEM_PROPERTY_PMD_COLOR = "pmd.color"; /** * Directory from where java was invoked. */ private String pwd; private String yellowBold = ""; private String whiteBold = ""; private String redBold = ""; private String cyan = ""; private String green = ""; private String colorReset = ""; public TextColorRenderer() { // This Renderer was originally submitted by Adrian Papari and was // called the "PapariTextRenderer" pre-PMD 5.0. super(NAME, "Text format, with color support (requires ANSI console support, e.g. xterm, rxvt, etc.)."); definePropertyDescriptor(COLOR); } @Override public String defaultFileExtension() { return "txt"; } /** * Enables colors on *nix systems - not windows. Color support depends on * the pmd.color property, which should be set with the -D option during * execution - a set value other than 'false' or '0' enables color. * <p/> * btw, is it possible to do this on windows (ie; console colors)? */ private void initializeColorsIfSupported() { if (isPropertyEnabled(getProperty(COLOR)) || isPropertyEnabled(System.getProperty(SYSTEM_PROPERTY_PMD_COLOR))) { this.yellowBold = "\u001B[1;33m"; this.whiteBold = "\u001B[1;37m"; this.redBold = "\u001B[1;31m"; this.green = "\u001B[0;32m"; this.cyan = "\u001B[0;36m"; this.colorReset = "\u001B[0m"; } } private boolean isPropertyEnabled(String property) { return property != null && !("0".equals(property) || "false".equalsIgnoreCase(property)); } /** * {@inheritDoc} */ @Override public void end() throws IOException { StringBuffer buf = new StringBuffer(500); buf.append(PMD.EOL); initializeColorsIfSupported(); String lastFile = null; int numberOfErrors = 0; int numberOfWarnings = 0; for (Iterator<RuleViolation> i = report.iterator(); i.hasNext();) { buf.setLength(0); numberOfWarnings++; RuleViolation rv = i.next(); if (!rv.getFilename().equals(lastFile)) { lastFile = rv.getFilename(); buf.append(this.yellowBold + "*" + this.colorReset + " file: " + this.whiteBold + this.getRelativePath(lastFile) + this.colorReset + PMD.EOL); } buf.append( this.green + " src: " + this.cyan + lastFile.substring(lastFile.lastIndexOf(File.separator) + 1) + this.colorReset + ":" + this.cyan + rv.getBeginLine() + (rv.getEndLine() == -1 ? "" : ":" + rv.getEndLine()) + this.colorReset + PMD.EOL); buf.append(this.green + " rule: " + this.colorReset + rv.getRule().getName() + PMD.EOL); buf.append(this.green + " msg: " + this.colorReset + rv.getDescription() + PMD.EOL); buf.append(this.green + " code: " + this.colorReset + this.getLine(lastFile, rv.getBeginLine()) + PMD.EOL + PMD.EOL); writer.write(buf.toString()); } writer.write(PMD.EOL + PMD.EOL); writer.write("Summary:" + PMD.EOL + PMD.EOL); Map<String, Integer> summary = report.getCountSummary(); for (Map.Entry<String, Integer> entry : summary.entrySet()) { buf.setLength(0); String key = entry.getKey(); buf.append(key).append(" : ").append(entry.getValue()).append(PMD.EOL); writer.write(buf.toString()); } for (Iterator<Report.ProcessingError> i = report.errors(); i.hasNext();) { buf.setLength(0); numberOfErrors++; Report.ProcessingError error = i.next(); if (error.getFile().equals(lastFile)) { lastFile = error.getFile(); buf.append(this.redBold + "*" + this.colorReset + " file: " + this.whiteBold + this.getRelativePath(lastFile) + this.colorReset + PMD.EOL); } buf.append(this.green + " err: " + this.cyan + error.getMsg() + this.colorReset + PMD.EOL + PMD.EOL); writer.write(buf.toString()); } // adding error message count, if any if (numberOfErrors > 0) { writer.write(this.redBold + "*" + this.colorReset + " errors: " + this.whiteBold + numberOfWarnings + this.colorReset + PMD.EOL); } writer.write(this.yellowBold + "*" + this.colorReset + " warnings: " + this.whiteBold + numberOfWarnings + this.colorReset + PMD.EOL); } /** * Retrieves the requested line from the specified file. * * @param sourceFile * the java or cpp source file * @param line * line number to extract * @return a trimmed line of source code */ private String getLine(String sourceFile, int line) { String code = null; BufferedReader br = null; try { br = new BufferedReader(getReader(sourceFile)); for (int i = 0; line > i; i++) { String txt = br.readLine(); code = txt == null ? "" : txt.trim(); } } catch (IOException ioErr) { ioErr.printStackTrace(); } finally { IOUtils.closeQuietly(br); } return code; } protected Reader getReader(String sourceFile) throws FileNotFoundException { return new FileReader(new File(sourceFile)); } /** * Attempts to determine the relative path to the file. If relative path * cannot be found, the original path is returnedi, ie - the current path * for the supplied file. * * @param fileName * well, the file with its original path. * @return the relative path to the file */ private String getRelativePath(String fileName) { String relativePath; // check if working directory need to be assigned if (pwd == null) { try { this.pwd = new File(".").getCanonicalPath(); } catch (IOException ioErr) { // to avoid further error this.pwd = ""; } } // make sure that strings match before doing any substring-ing if (fileName.indexOf(this.pwd) == 0) { relativePath = "." + fileName.substring(this.pwd.length()); // remove current dir occuring twice - occurs if . was supplied as // path if (relativePath.startsWith("." + File.separator + "." + File.separator)) { relativePath = relativePath.substring(2); } } else { // this happens when pmd's supplied argument deviates from the pwd // 'branch' (god knows this terminolgy - i hope i make some sense). // for instance, if supplied=/usr/lots/of/src and // pwd=/usr/lots/of/shared/source // TODO: a fix to get relative path? relativePath = fileName; } return relativePath; } }