View Javadoc

1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.renderers;
5   
6   import java.io.BufferedReader;
7   import java.io.File;
8   import java.io.FileNotFoundException;
9   import java.io.FileReader;
10  import java.io.IOException;
11  import java.io.Reader;
12  import java.util.Iterator;
13  import java.util.Map;
14  
15  import net.sourceforge.pmd.PMD;
16  import net.sourceforge.pmd.Report;
17  import net.sourceforge.pmd.RuleViolation;
18  import net.sourceforge.pmd.lang.rule.properties.StringProperty;
19  
20  import org.apache.commons.io.IOUtils;
21  
22  /**
23   * <p>A console renderer with optional color support under *nix systems.</p>
24   * <p/>
25   * <pre>
26   * * file: ./src/gilot/Test.java
27   *     src:  Test.java:12
28   *     rule: AtLeastOneConstructor
29   *     msg:  Each class should declare at least one constructor
30   *     code: public class Test
31   * <p/>
32   * * file: ./src/gilot/log/format/LogInterpreter.java
33   *     src:  LogInterpreter.java:317
34   *     rule: AvoidDuplicateLiterals
35   *     msg:  The same String literal appears 4 times in this file; the first occurrence is on line 317
36   *     code: logger.error( "missing attribute 'app_arg' in rule '" + ((Element)element.getParent()).getAttributeValue( "name" ) + "'" );
37   * <p/>
38   *     src:  LogInterpreter.java:317
39   *     rule: AvoidDuplicateLiterals
40   *     msg:  The same String literal appears 5 times in this file; the first occurrence is on line 317
41   *     code: logger.error( "missing attribute 'app_arg' in rule '" + ((Element)element.getParent()).getAttributeValue( "name" ) + "'" );
42   * <p/>
43   * * warnings: 3
44   * <p/>
45   * </pre>
46   * <p/>
47   * <p>Colorization is turned on by supplying -D<b>pmd.color</b> - any value other than
48   * '0' or 'false', enables color - including an empty value (''). <b>Nota Bene:</b>
49   * colorization is atm only supported under *nix terminals accepting ansi escape
50   * sequences, such as xterm, rxvt et cetera.</p>
51   */
52  public class TextColorRenderer extends AbstractAccumulatingRenderer {
53  
54  
55      public static final String NAME = "textcolor";
56  
57      public static final StringProperty COLOR = new StringProperty("color", "Enables colors with anything other than 'false' or '0'.", "yes", 0);
58      private static final String SYSTEM_PROPERTY_PMD_COLOR = "pmd.color";
59  
60      /**
61       * Directory from where java was invoked.
62       */
63      private String pwd;
64  
65      private String yellowBold = "";
66      private String whiteBold = "";
67      private String redBold = "";
68      private String cyan = "";
69      private String green = "";
70  
71      private String colorReset = "";
72  
73      public TextColorRenderer() {
74  	// This Renderer was originally submitted by Adrian Papari and was called the "PapariTextRenderer" pre-PMD 5.0.
75  	super(NAME, "Text format, with color support (requires ANSI console support, e.g. xterm, rxvt, etc.).");
76  	definePropertyDescriptor(COLOR);
77      }
78  
79      public String defaultFileExtension() { return "txt"; }
80      
81      /**
82       * Enables colors on *nix systems - not windows. Color support depends
83       * on the pmd.color property, which should be set with the -D option
84       * during execution - a set value other than 'false' or '0' enables color.
85       * <p/>
86       * btw, is it possible to do this on windows (ie; console colors)?
87       */
88      private void initializeColorsIfSupported() {
89  	if (isPropertyEnabled(getProperty(COLOR)) || isPropertyEnabled(System.getProperty(SYSTEM_PROPERTY_PMD_COLOR))) {
90  	    this.yellowBold = "\u001B[1;33m";
91  	    this.whiteBold = "\u001B[1;37m";
92  	    this.redBold = "\u001B[1;31m";
93  	    this.green = "\u001B[0;32m";
94  	    this.cyan = "\u001B[0;36m";
95  
96  	    this.colorReset = "\u001B[0m";
97  	}
98      }
99  
100     private boolean isPropertyEnabled(String property) {
101 	return property != null && !(property.equals("0") || property.equalsIgnoreCase("false"));
102     }
103 
104     /**
105      * {@inheritDoc}
106      */
107     @Override
108     public void end() throws IOException {
109 	StringBuffer buf = new StringBuffer(500);
110 	buf.append(PMD.EOL);
111 	initializeColorsIfSupported();
112 	String lastFile = null;
113 	int numberOfErrors = 0;
114 	int numberOfWarnings = 0;
115 
116 	for (Iterator<RuleViolation> i = report.iterator(); i.hasNext();) {
117 	    buf.setLength(0);
118 	    numberOfWarnings++;
119 	    RuleViolation rv = i.next();
120 	    if (!rv.getFilename().equals(lastFile)) {
121 		lastFile = rv.getFilename();
122 		buf.append(this.yellowBold + "*" + this.colorReset + " file: " + this.whiteBold
123 			+ this.getRelativePath(lastFile) + this.colorReset + PMD.EOL);
124 	    }
125 	    buf.append(this.green + "    src:  " + this.cyan
126 		    + lastFile.substring(lastFile.lastIndexOf(File.separator) + 1) + this.colorReset + ":" + this.cyan
127 		    + rv.getBeginLine() + (rv.getEndLine() == -1 ? "" : ":" + rv.getEndLine()) + this.colorReset
128 		    + PMD.EOL);
129 	    buf.append(this.green + "    rule: " + this.colorReset + rv.getRule().getName() + PMD.EOL);
130 	    buf.append(this.green + "    msg:  " + this.colorReset + rv.getDescription() + PMD.EOL);
131 	    buf.append(this.green + "    code: " + this.colorReset + this.getLine(lastFile, rv.getBeginLine())
132 		    + PMD.EOL + PMD.EOL);
133 	    writer.write(buf.toString());
134 	}
135 	writer.write(PMD.EOL + PMD.EOL);
136 	writer.write("Summary:" + PMD.EOL + PMD.EOL);
137 	Map<String, Integer> summary = report.getCountSummary();
138 	for (Map.Entry<String, Integer> entry : summary.entrySet()) {
139 	    buf.setLength(0);
140 	    String key = entry.getKey();
141 	    buf.append(key).append(" : ").append(entry.getValue()).append(PMD.EOL);
142 	    writer.write(buf.toString());
143 	}
144 
145 	for (Iterator<Report.ProcessingError> i = report.errors(); i.hasNext();) {
146 	    buf.setLength(0);
147 	    numberOfErrors++;
148 	    Report.ProcessingError error = i.next();
149 	    if (error.getFile().equals(lastFile)) {
150 		lastFile = error.getFile();
151 		buf.append(this.redBold + "*" + this.colorReset + " file: " + this.whiteBold
152 			+ this.getRelativePath(lastFile) + this.colorReset + PMD.EOL);
153 	    }
154 	    buf.append(this.green + "    err:  " + this.cyan + error.getMsg() + this.colorReset + PMD.EOL + PMD.EOL);
155 	    writer.write(buf.toString());
156 	}
157 
158 	// adding error message count, if any
159 	if (numberOfErrors > 0) {
160 	    writer.write(this.redBold + "*" + this.colorReset + " errors:   " + this.whiteBold + numberOfWarnings
161 		    + this.colorReset + PMD.EOL);
162 	}
163 	writer.write(this.yellowBold + "*" + this.colorReset + " warnings: " + this.whiteBold + numberOfWarnings
164 		+ this.colorReset + PMD.EOL);
165     }
166 
167     /**
168      * Retrieves the requested line from the specified file.
169      *
170      * @param sourceFile the java or cpp source file
171      * @param line       line number to extract
172      * @return a trimmed line of source code
173      */
174     private String getLine(String sourceFile, int line) {
175 		String code = null;
176 		BufferedReader br = null;
177 		try {
178 		    br = new BufferedReader(getReader(sourceFile));
179 		    for (int i = 0; line > i; i++) {
180 		    	String txt = br.readLine();
181 				code = txt == null ? "" : txt.trim();
182 			    }
183 		} catch (IOException ioErr) {
184 		    ioErr.printStackTrace();
185 		} finally {
186 			IOUtils.closeQuietly(br);
187 		}
188 		return code;
189     }
190 
191     protected Reader getReader(String sourceFile) throws FileNotFoundException {
192 	return new FileReader(new File(sourceFile));
193     }
194 
195     /**
196      * Attempts to determine the relative path to the file. If relative path cannot be found,
197      * the original path is returnedi, ie - the current path for the supplied file.
198      *
199      * @param fileName well, the file with its original path.
200      * @return the relative path to the file
201      */
202     private String getRelativePath(String fileName) {
203 	String relativePath;
204 
205 	// check if working directory need to be assigned
206 	if (pwd == null) {
207 	    try {
208 		this.pwd = new File(".").getCanonicalPath();
209 	    } catch (IOException ioErr) {
210 		// to avoid further error
211 		this.pwd = "";
212 	    }
213 	}
214 
215 	// make sure that strings match before doing any substring-ing
216 	if (fileName.indexOf(this.pwd) == 0) {
217 	    relativePath = "." + fileName.substring(this.pwd.length());
218 
219 	    // remove current dir occuring twice - occurs if . was supplied as path
220 	    if (relativePath.startsWith("." + File.separator + "." + File.separator)) {
221 		relativePath = relativePath.substring(2);
222 	    }
223 	} else {
224 	    // this happens when pmd's supplied argument deviates from the pwd 'branch' (god knows this terminolgy - i hope i make some sense).
225 	    // for instance, if supplied=/usr/lots/of/src and pwd=/usr/lots/of/shared/source
226 	    // TODO: a fix to get relative path?
227 	    relativePath = fileName;
228 	}
229 
230 	return relativePath;
231     }
232 }