TextColorRenderer.java
001 /**
002  * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003  */
004 package net.sourceforge.pmd.renderers;
005 
006 import java.io.BufferedReader;
007 import java.io.File;
008 import java.io.FileNotFoundException;
009 import java.io.FileReader;
010 import java.io.IOException;
011 import java.io.Reader;
012 import java.util.Iterator;
013 import java.util.Map;
014 import java.util.Properties;
015 
016 import net.sourceforge.pmd.PMD;
017 import net.sourceforge.pmd.Report;
018 import net.sourceforge.pmd.RuleViolation;
019 
020 /**
021  <p>A console renderer with optional color support under *nix systems.</p>
022  <p/>
023  <pre>
024  * * file: ./src/gilot/Test.java
025  *     src:  Test.java:12
026  *     rule: AtLeastOneConstructor
027  *     msg:  Each class should declare at least one constructor
028  *     code: public class Test
029  <p/>
030  * * file: ./src/gilot/log/format/LogInterpreter.java
031  *     src:  LogInterpreter.java:317
032  *     rule: AvoidDuplicateLiterals
033  *     msg:  The same String literal appears 4 times in this file; the first occurrence is on line 317
034  *     code: logger.error( "missing attribute 'app_arg' in rule '" + ((Element)element.getParent()).getAttributeValue( "name" ) + "'" );
035  <p/>
036  *     src:  LogInterpreter.java:317
037  *     rule: AvoidDuplicateLiterals
038  *     msg:  The same String literal appears 5 times in this file; the first occurrence is on line 317
039  *     code: logger.error( "missing attribute 'app_arg' in rule '" + ((Element)element.getParent()).getAttributeValue( "name" ) + "'" );
040  <p/>
041  * * warnings: 3
042  <p/>
043  </pre>
044  <p/>
045  <p>Colorization is turned on by supplying -D<b>pmd.color</b> - any value other than
046  * '0' or 'false', enables color - including an empty value (''). <b>Nota Bene:</b>
047  * colorization is atm only supported under *nix terminals accepting ansi escape
048  * sequences, such as xterm, rxvt et cetera.</p>
049  */
050 public class TextColorRenderer extends AbstractAccumulatingRenderer {
051 
052     public static final String NAME = "textcolor";
053 
054     public static final String COLOR = "color";
055 
056     /**
057      * Directory from where java was invoked.
058      */
059     private String pwd;
060 
061     private String yellowBold = "";
062     private String whiteBold = "";
063     private String redBold = "";
064     private String cyan = "";
065     private String green = "";
066 
067     private String colorReset = "";
068 
069     public TextColorRenderer(Properties properties) {
070   // This Renderer was originally submitted by Adrian Papari and was called the "PapariTextRenderer" pre-PMD 5.0.
071   super(NAME, "Text format, with color support (requires ANSI console support, e.g. xterm, rxvt, etc.).",
072     properties);
073   defineProperty(COLOR, "Enables colors with anything other than 'false' or '0'.");
074     }
075 
076     /**
077      * Enables colors on *nix systems - not windows. Color support depends
078      * on the pmd.color property, which should be set with the -D option
079      * during execution - a set value other than 'false' or '0' enables color.
080      <p/>
081      * btw, is it possible to do this on windows (ie; console colors)?
082      */
083     private void initializeColorsIfSupported() {
084   if (System.getProperty("pmd.color"!= null
085     && !(System.getProperty("pmd.color").equals("0"|| System.getProperty("pmd.color").equals("false"))) {
086       this.yellowBold = "\u001B[1;33m";
087       this.whiteBold = "\u001B[1;37m";
088       this.redBold = "\u001B[1;31m";
089       this.green = "\u001B[0;32m";
090       this.cyan = "\u001B[0;36m";
091 
092       this.colorReset = "\u001B[0m";
093   }
094     }
095 
096     /**
097      * {@inheritDoc}
098      */
099     @Override
100     public void end() throws IOException {
101   StringBuffer buf = new StringBuffer(500);
102   buf.append(PMD.EOL);
103   initializeColorsIfSupported();
104   String lastFile = null;
105   int numberOfErrors = 0;
106   int numberOfWarnings = 0;
107 
108   for (Iterator<RuleViolation> i = report.iterator(); i.hasNext();) {
109       buf.setLength(0);
110       numberOfWarnings++;
111       RuleViolation rv = i.next();
112       if (!rv.getFilename().equals(lastFile)) {
113     lastFile = rv.getFilename();
114     buf.append(this.yellowBold + "*" this.colorReset + " file: " this.whiteBold
115       this.getRelativePath(lastFilethis.colorReset + PMD.EOL);
116       }
117       buf.append(this.green + "    src:  " this.cyan
118         + lastFile.substring(lastFile.lastIndexOf(File.separator1this.colorReset + ":" this.cyan
119         + rv.getBeginLine() (rv.getEndLine() == -"" ":" + rv.getEndLine()) this.colorReset
120         + PMD.EOL);
121       buf.append(this.green + "    rule: " this.colorReset + rv.getRule().getName() + PMD.EOL);
122       buf.append(this.green + "    msg:  " this.colorReset + rv.getDescription() + PMD.EOL);
123       buf.append(this.green + "    code: " this.colorReset + this.getLine(lastFile, rv.getBeginLine())
124         + PMD.EOL + PMD.EOL);
125       writer.write(buf.toString());
126   }
127   writer.write(PMD.EOL + PMD.EOL);
128   writer.write("Summary:" + PMD.EOL + PMD.EOL);
129   Map<String, Integer> summary = report.getCountSummary();
130   for (Map.Entry<String, Integer> entry : summary.entrySet()) {
131       buf.setLength(0);
132       String key = entry.getKey();
133       buf.append(key).append(" : ").append(entry.getValue()).append(PMD.EOL);
134       writer.write(buf.toString());
135   }
136 
137   for (Iterator<Report.ProcessingError> i = report.errors(); i.hasNext();) {
138       buf.setLength(0);
139       numberOfErrors++;
140       Report.ProcessingError error = i.next();
141       if (error.getFile().equals(lastFile)) {
142     lastFile = error.getFile();
143     buf.append(this.redBold + "*" this.colorReset + " file: " this.whiteBold
144       this.getRelativePath(lastFilethis.colorReset + PMD.EOL);
145       }
146       buf.append(this.green + "    err:  " this.cyan + error.getMsg() this.colorReset + PMD.EOL + PMD.EOL);
147       writer.write(buf.toString());
148   }
149 
150   // adding error message count, if any
151   if (numberOfErrors > 0) {
152       writer.write(this.redBold + "*" this.colorReset + " errors:   " this.whiteBold + numberOfWarnings
153         this.colorReset + PMD.EOL);
154   }
155   writer.write(this.yellowBold + "*" this.colorReset + " warnings: " this.whiteBold + numberOfWarnings
156     this.colorReset + PMD.EOL);
157     }
158 
159     /**
160      * Retrieves the requested line from the specified file.
161      *
162      @param sourceFile the java or cpp source file
163      @param line       line number to extract
164      @return a trimmed line of source code
165      */
166     private String getLine(String sourceFile, int line) {
167   String code = null;
168   BufferedReader br = null;
169   try {
170       br = new BufferedReader(getReader(sourceFile));
171       for (int i = 0; line > i; i++) {
172     code = br.readLine().trim();
173       }
174   catch (IOException ioErr) {
175       ioErr.printStackTrace();
176   finally {
177       if (br != null) {
178     try {
179         br.close();
180     catch (IOException ioErr) {
181         ioErr.printStackTrace();
182     }
183       }
184   }
185   return code;
186     }
187 
188     protected Reader getReader(String sourceFilethrows FileNotFoundException {
189   return new FileReader(new File(sourceFile));
190     }
191 
192     /**
193      * Attempts to determine the relative path to the file. If relative path cannot be found,
194      * the original path is returnedi, ie - the current path for the supplied file.
195      *
196      @param fileName well, the file with its original path.
197      @return the relative path to the file
198      */
199     private String getRelativePath(String fileName) {
200   String relativePath;
201 
202   // check if working directory need to be assigned
203   if (pwd == null) {
204       try {
205     this.pwd = new File(".").getCanonicalPath();
206       catch (IOException ioErr) {
207     // to avoid further error
208     this.pwd = "";
209       }
210   }
211 
212   // make sure that strings match before doing any substring-ing
213   if (fileName.indexOf(this.pwd== 0) {
214       relativePath = "." + fileName.substring(this.pwd.length());
215 
216       // remove current dir occuring twice - occurs if . was supplied as path
217       if (relativePath.startsWith("." + File.separator + "." + File.separator)) {
218     relativePath = relativePath.substring(2);
219       }
220   else {
221       // this happens when pmd's supplied argument deviates from the pwd 'branch' (god knows this terminolgy - i hope i make some sense).
222       // for instance, if supplied=/usr/lots/of/src and pwd=/usr/lots/of/shared/source
223       // TODO: a fix to get relative path?
224       relativePath = fileName;
225   }
226 
227   return relativePath;
228     }
229 }