001: package tide.exttools.PMD;
002:
003: import tide.sources.SourceFile;
004: import org.xml.sax.*;
005: import org.xml.sax.helpers.*;
006: import org.xml.sax.SAXException;
007: import javax.xml.parsers.SAXParser;
008: import javax.xml.parsers.SAXParserFactory;
009: import tide.editor.linemessages.*;
010: import tide.editor.*;
011: import tide.project.*;
012: import tide.utils.*;
013: import snow.utils.gui.*;
014: import snow.utils.storage.*;
015: import snow.utils.*;
016: import javax.swing.*;
017: import javax.swing.event.*;
018: import javax.swing.border.*;
019: import java.awt.event.*;
020: import java.io.*;
021: import java.util.*;
022: import java.util.regex.*;
023: import java.util.jar.JarEntry;
024: import java.util.jar.JarFile;
025: import javax.xml.transform.stream.StreamSource;
026:
027: /** PMD is nice and quick.
028: */
029: public final class PMDLauncher// implements ChangedFilesSaveListener
030: {
031: private static PMDLauncher instance = null;
032:
033: private PMDLauncher() {
034: }
035:
036: public static PMDLauncher getInstance() {
037: if (instance == null)
038: instance = new PMDLauncher();
039: return instance;
040: }
041:
042: /** Should be called at startup and each project settings changes and load ops
043: *
044: public void configureAutoScan()
045: {
046: ProjectSettings settings = MainEditorFrame.instance.getActualProject();
047: boolean isAuto = settings.getBooleanProperty("pmd_autoApplyOnChanges",false) && PMDSettingsDialog.isConfigured();
048:
049:
050: if(isAuto)
051: {
052: // added only once !
053: MainEditorFrame.instance.editorPanel.addChangedFilesSaveListener(this);
054: }
055: else
056: {
057: MainEditorFrame.instance.editorPanel.removeChangedFilesSaveListener(this);
058: }
059: }*/
060:
061: /** ChangedFilesSaveListener interface.
062: */
063: public void filesSaved(final Set<SourceFile> sources) {
064: if (!PMDSettingsDialog.isConfigured())
065: return;
066:
067: // Enforce EDT, because we are not necessary beeing called from edt here
068: final ProgressModalDialog[] pmd = new ProgressModalDialog[1];
069: SwingSafeRunnable.invokeAndWait(new Runnable() {
070: public void run() { // MUST WAIT HERE!
071: pmd[0] = new ProgressModalDialog(
072: MainEditorFrame.instance, "PMD autoanalysis ("
073: + sources.size() + " file"
074: + (sources.size() == 1 ? "" : "s")
075: + ")", false);
076: pmd[0].startDelayed(30000L); // big delay, normally never visible...
077: }
078: });
079:
080: Thread t = new Thread() {
081: public void run() {
082: try {
083: _analyse(true, pmd[0], sources
084: .toArray(new SourceFile[sources.size()])); // Silent
085: /*for(SourceFile sf : sources)
086: {
087: if(pmd.getWasCancelled()) break;
088: _analyse(sf, true, pmd); // Silent
089: }*/
090: } catch (Exception ex) {
091: ex.printStackTrace();
092: } finally {
093: pmd[0].closeDialog();
094: }
095: }
096: };
097: t.start();
098: }
099:
100: public static void _analyse(boolean silentMode,
101: ProgressModalDialog pmd, SourceFile... sfs)
102: throws Exception {
103:
104: if (sfs.length == 1) {
105: SourceFile sf = sfs[0];
106: if (sf.isSourceFile()) {
107: LineMessagesManager.getInstance().removeMessagesFor(
108: sf.getJavaName(), PMDMessage.class);
109: analyse(silentMode, pmd, Arrays.asList(sf.javaFile));
110: } else if (sf.isDirectory()) {
111: String pn = sf.getPackageName();
112: if (pn.length() > 0)
113: pn += ".";
114: LineMessagesManager.getInstance()
115: .removeMessagesForJavaNameStartingWith(pn,
116: PMDMessage.class);
117: File d = new File(MainEditorFrame.instance
118: .getActualProject().getSources_Home(), sf
119: .getJavaName().replace(".", "/"));
120: analyse(silentMode, pmd, Arrays.asList(d));
121: }
122: } else {
123: List<File> lf = new ArrayList<File>();
124: for (SourceFile sfi : sfs) {
125: LineMessagesManager.getInstance().removeMessagesFor(
126: sfi.getJavaName(), PMDMessage.class);
127: lf.add(sfi.javaFile);
128: }
129: analyse(silentMode, pmd, lf);
130: }
131: }
132:
133: /** As argument: either a source or a directory.
134: * must be called in a separate thread
135: */
136: private static void analyse(boolean silentMode,
137: ProgressModalDialog pmd, List<File> sourcesToAnalyse)
138: throws Exception {
139: ProjectSettings actualProject = MainEditorFrame.instance
140: .getActualProject();
141:
142: boolean ignoreIrrelevant = actualProject.getBooleanProperty(
143: "pmd_ignoreIrrelevant", false);
144:
145: File pmdJar = new File(actualProject.getProperty("PMD_path",
146: PMDSettingsDialog.defaultPMDLocation));
147: if (!pmdJar.exists())
148: throw new Exception("pmd jar not found at "
149: + pmdJar.getAbsolutePath());
150:
151: File javaExePath = actualProject.getJava_TOOL();
152: String options = actualProject.getProperty("PMD_Options",
153: PMDSettingsDialog.defaultOptions);
154:
155: List<String> execCommand = new ArrayList<String>();
156: execCommand.add(javaExePath.getAbsolutePath());
157: execCommand.add("-jar");
158: execCommand.add(pmdJar.getAbsolutePath());
159:
160: // 1: source (file or dir (recurse) )
161: if (sourcesToAnalyse.size() == 1) {
162: execCommand.add(sourcesToAnalyse.get(0).getAbsolutePath());
163: } else {
164: // comma "," separated (as saw in sourcecode of PMD)
165: StringBuilder sb = new StringBuilder();
166: for (int i = 0; i < sourcesToAnalyse.size() - 1; i++) {
167: sb.append(sourcesToAnalyse.get(i).getAbsolutePath());
168: sb.append(",");
169: }
170: sb.append(sourcesToAnalyse.get(sourcesToAnalyse.size() - 1)
171: .getAbsolutePath());
172: execCommand.add(sb.toString());
173: }
174:
175: // 2: report format
176: execCommand.add("xml"); // html, xml
177:
178: // 3: rules list
179: String rules = actualProject.getProperty("PMD_UsedRules",
180: PMDSettingsDialog.recommandedPMDRules).trim();
181: rules = rules.replace(" ", ","); // remove spaces
182: rules = rules.replace("\n", ","); // and cr lf
183: rules = rules.replace("\r", ""); //
184: rules = rules.replace(",,", ","); // remove double ,
185:
186: if (rules.toLowerCase().indexOf(".xml/") < 0) {
187: // OLD Style: nothing special specified...
188: execCommand.add(rules);
189: } else {
190: // we have "subrules" => create
191: File tf = new File(
192: actualProject.getProjectSettingsFolder(),
193: "pmdcustom.xml");
194: tf.deleteOnExit();
195: FileUtils.saveToFile(generateXMLCustomRuleSet(rules), tf);
196: execCommand.add(tf.getAbsolutePath());
197: }
198:
199: // 4: opts
200: if (options != null && options.trim().length() > 0) {
201: execCommand.addAll(ProjectUtils.splitArgs(options, false));
202: }
203:
204: if (!silentMode) {
205: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
206: .clearDocument();
207: MainEditorFrame.instance.outputPanels.selectToolsTab(false);
208: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
209: .appendDatedLine("PMD analysis of "
210: + sourcesToAnalyse + ":\r\n");
211: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
212: .appendLine("Used rules: " + rules);
213:
214: }
215:
216: MainEditorFrame.debugOut("Running PMD: " + execCommand);
217: Process proc = new ProcessBuilder(execCommand).start();
218:
219: pmd.setProcessToOptionalKill(proc);
220: if (!silentMode)
221: pmd.start();
222:
223: if (!silentMode) {
224: MainEditorFrame.instance.outputPanels.processesManager
225: .addProcess("PMD analysis of " + sourcesToAnalyse
226: + "", proc, true);
227: }
228:
229: // write the analysis result in a file
230: File file = File
231: .createTempFile("tIDE_pmd_analysis.tmp", ".xml"); //"temp/pmd_temp_res.xml");
232: file.deleteOnExit();
233:
234: FileOutputStream fos = new FileOutputStream(file);
235: StreamGobbler sg = new StreamGobbler(proc.getInputStream(), fos);
236: //MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.createWriterForThisDocument(false), "");
237: sg.start();
238:
239: // errors
240: StreamGobbler sge = new StreamGobbler(
241: proc.getErrorStream(),
242: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
243: .createWriterForThisDocument(true), "PMD");
244: sge.start();
245:
246: // wait until proc completed...
247: proc.waitFor();
248:
249: fos.close();
250:
251: // XSLT
252: String xsltRelPath = "tide/exttools/PMD/simple_pmd_report.xsl";
253: InputStream is = PMDLauncher.class.getClassLoader()
254: .getResourceAsStream(xsltRelPath); // jar mode
255: if (is == null) {
256: // IDE mode (develop)
257: //System.out.println("no PMD xsl found in jar");
258: File fi = new File(xsltRelPath);
259: if (fi.exists()) {
260: is = new FileInputStream(fi);
261: } else {
262: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
263: .appendError("\nXSLT transform file not found in "
264: + xsltRelPath);
265: System.out.println("File not found: " + fi);
266: }
267: }
268:
269: if (is != null) {
270: try {
271: String res = XMLUtils.transformXML(is,
272: new StreamSource(file));
273:
274: Map[] stats = parseMessages(res, ignoreIrrelevant);
275:
276: if (!silentMode) {
277: @SuppressWarnings("unchecked")
278: Map<String, Integer> catCounts = (Map<String, Integer>) stats[0];
279: @SuppressWarnings("unchecked")
280: Map<Integer, Integer> priorCounts = (Map<Integer, Integer>) stats[1];
281: int tot = 0;
282: for (Integer ci : catCounts.values()) {
283: tot += ci;
284: }
285: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
286: .appendLine("\n"
287: + tot
288: + " messages generated (see Messages tab)");
289:
290: // Stats print
291: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
292: .appendLine("\n"
293: + priorCounts.size()
294: + " priorit"
295: + (priorCounts.size() == 1 ? "y"
296: : "ies") + ":");
297: ArrayList<Integer> prior = new ArrayList<Integer>(
298: priorCounts.keySet());
299: Collections.sort(prior);
300:
301: for (Integer pi : prior) {
302: int pic = priorCounts.get(pi);
303: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
304: .appendLine("\tpriority " + pi
305: + ": \t" + pic + " message"
306: + (pic == 1 ? "" : "s"));
307: }
308:
309: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
310: .appendLine("\n"
311: + catCounts.size()
312: + " categor"
313: + (catCounts.size() == 1 ? "y"
314: : "ies") + ":");
315: ArrayList<String> cats = new ArrayList<String>(
316: catCounts.keySet());
317: Collections.sort(cats);
318:
319: for (String cai : cats) {
320: int ci = catCounts.get(cai);
321: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
322: .appendLine("\t" + ci + "\t " + cai);
323: }
324: }
325: } catch (Exception e) {
326: // occurs often during auto analysis, when the file is not "compilable" and has been saved...
327: // ex: javax.xml.transform.TransformerException: com.sun.org.apache.xml.internal.utils.WrappedRuntimeException: Premature end of file.
328: if (!silentMode) {
329: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
330: .appendError("ERROR during PMD XSLT transform: "
331: + e.getMessage() + "\n");
332: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
333: .appendLine(FileUtils
334: .getFileStringContent(file));
335: //e.printStackTrace();
336: }
337: } finally {
338: is.close();
339: }
340: } else {
341: MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc
342: .appendLine(FileUtils.getFileStringContent(file));
343: }
344:
345: if (!silentMode) {
346: MainEditorFrame.instance.outputPanels.toolsOutputPanel
347: .setCaret0();
348: }
349: LineMessagesManager.getInstance().refreshView();
350: }
351:
352: /** The formatted output is generated from the transformed pmd xml output.
353: * the format is some white line separated messages ending with the source position.
354: */
355: private static Map[] parseMessages(String formattedOutput,
356: boolean ignoreIrrelevant) throws Exception {
357: Map<String, Integer> catCountStat = new HashMap<String, Integer>();
358: Map<Integer, Integer> priorityCount = new HashMap<Integer, Integer>();
359:
360: String pa = MainEditorFrame.instance.getActualProject()
361: .getSources_Home().getAbsolutePath();
362: if (!pa.endsWith("\\"))
363: pa += "\\";
364:
365: BufferedReader sr = new BufferedReader(new StringReader(
366: formattedOutput));
367: StringBuilder actualMess = new StringBuilder();
368: String line = null;
369: int count = 0;
370: wl: while ((line = sr.readLine()) != null) {
371: //System.out.println(""+line);
372: if (line.startsWith("===")) // separate sources
373: {
374: actualMess.setLength(0);
375: continue wl;
376: }
377:
378: actualMess.append("\n" + line);
379:
380: if (line.startsWith(" at ")) {
381: String javaName = line.substring(4 + pa.length());
382: int pos = javaName.indexOf(".java:");
383: int lineNr = -1;
384: int colNr = -1;
385: if (pos > 0) {
386: String[] lns = javaName.substring(pos + 6).split(
387: ":");
388: javaName = javaName.substring(0, pos);
389: if (lns != null && lns.length > 0
390: && lns[0].length() > 0) {
391: lineNr = Integer.parseInt(lns[0]);
392: if (lns.length > 1) {
393: colNr = Integer.parseInt(lns[1]) - 1;
394: }
395: }
396: }
397: javaName = javaName.replace("\\", ".");
398:
399: PMDMessage pmm = PMDMessage.createAndAdd(javaName,
400: actualMess.toString().trim(), lineNr, colNr,
401: ignoreIrrelevant);
402: count++;
403: actualMess.setLength(0);
404: String cat = pmm.getCategory();
405:
406: if (LineMessagesManager.getInstance()
407: .getIrrelevantCategories().contains(cat)) {
408: // TODO: also include a stat of ignored...
409: continue;
410: }
411:
412: // stats.
413:
414: if (!catCountStat.containsKey(cat)) {
415: catCountStat.put(cat, 1);
416: } else {
417: catCountStat.put(cat, catCountStat.get(cat) + 1);
418: }
419:
420: int prior = pmm.getPriority();
421: if (!priorityCount.containsKey(prior)) {
422: priorityCount.put(prior, 1);
423: } else {
424: priorityCount.put(prior,
425: priorityCount.get(prior) + 1);
426: }
427:
428: }
429: }
430: return new Map[] { catCountStat, priorityCount };
431: }
432:
433: public static String generateXMLCustomRuleSet(String rulesList) {
434: StringBuilder sb = new StringBuilder();
435: sb.append("<?xml version=\"1.0\"?>");
436: sb.append("\n<ruleset name=\"tIDE custom PMD ruleset\"");
437: sb.append("\nxmlns=\"http://pmd.sf.net/ruleset/1.0.0\"");
438: sb
439: .append("\nxmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
440: sb
441: .append("\nxsi:schemaLocation=\"http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd\"");
442: sb
443: .append("\nxsi:noNamespaceSchemaLocation=\"http://pmd.sf.net/ruleset_xml_schema.xsd\">");
444: sb.append("\n\n<description>");
445: sb.append("\nThis ruleset contains the PMD rules used by tIDE");
446: sb.append("\n</description>\n");
447:
448: for (String ri : rulesList.split(",")) {
449: sb.append("\n<rule ref=\"" + ri.trim() + "\"/>");
450: }
451:
452: sb.append("\n</ruleset>");
453: return sb.toString();
454: }
455:
456: /** read all rules from jar.
457: * in the rulesets/ directory
458: */
459: public static RuleSet readRulesetsNames(File pmdJarFile,
460: boolean withrules) throws Exception {
461: ArrayList<String> rs = new ArrayList<String>();
462: JarFile jf = null;
463: RuleSet root = new RuleSet("", "", "");
464: try {
465: jf = new JarFile(pmdJarFile);
466: Enumeration<JarEntry> entries = jf.entries();
467: while (entries.hasMoreElements()) {
468: JarEntry je = entries.nextElement();
469: if (je.getName().startsWith("rulesets/")) {
470: String en = je.getName();
471: if (en.toLowerCase().endsWith(".xml")) {
472: rs.add(en);
473:
474: if (withrules) {
475: String cont = FileUtils.getStringContent(jf
476: .getInputStream(je));
477: RuleSet ruse = parseRules(en, cont);
478: root.add(ruse);
479: }
480: }
481:
482: }
483: }
484: } catch (Exception e) {
485: throw e;
486: } finally {
487: jf.close();
488: }
489: return root;
490: }
491:
492: static int nn = 0;
493:
494: // minor bug: 38 != rulesets/releases/39.xml is named 38
495: private static RuleSet parseRules(String fullName, String cont) {
496: // Use the default (non-validating) parser
497: SAXParserFactory factory = SAXParserFactory.newInstance();
498:
499: // Parse the file
500: try {
501: SAXParser saxParser = factory.newSAXParser();
502:
503: InputSource is = new InputSource(new StringReader(cont));
504: RulesetParser rp = new RulesetParser();
505: saxParser.parse(is, rp);
506: RuleSet rs = new RuleSet(fullName, rp.rulesetName,
507: rp.rulesetDescription);
508:
509: // verification:
510: /*
511: int pos = fullName.lastIndexOf('/');
512: String name = fullName.substring(pos+1, fullName.length()-4);
513: if(!name.equals(rp.rulesetName))
514: {
515: //System.out.println("Ruleset name mismatch xml file name: "+rp.rulesetName+" != "+fullName);
516: }*/
517:
518: for (Rule r : rp.rules) {
519: rs.add(r);
520: }
521:
522: return rs;
523: } catch (Exception ex) {
524: ex.printStackTrace();
525: System.out.println("Cannot parse <" + cont + ">");
526: throw new RuntimeException("cnp");
527: }
528: }
529:
530: /* public static void main(String[] aa)
531: {
532: PMDSettingsDialog.main(aa);
533: }*/
534:
535: static class RulesetParser extends DefaultHandler {
536:
537: List<Rule> rules = new ArrayList<Rule>();
538: String rulesetName;
539: String rulesetDescription;
540:
541: boolean isInRule = false;
542: StringBuilder lastChars = new StringBuilder();
543: Rule actualRule = null;
544:
545: @Override
546: public void startDocument() {
547: }
548:
549: @Override
550: public void endDocument() {
551: }
552:
553: void pa(Attributes at) {
554: for (int i = 0; i < at.getLength(); i++) {
555: System.out.print("" + at.getLocalName(i) + ": "
556: + at.getValue(i) + ", ");
557: }
558: System.out.println("");
559: }
560:
561: @Override
562: public void startElement(String namespaceURI, String sName,
563: String qName, Attributes attributes)
564: throws SAXException {
565: // reset
566: lastChars.setLength(0);
567:
568: //sn is empty.
569: //pa(attributes);
570: if (qName.equalsIgnoreCase("ruleset")) {
571: rulesetName = attributes.getValue("name");
572: } else if (qName.equalsIgnoreCase("rule")) {
573: isInRule = true;
574: actualRule = new Rule();
575: actualRule.setName(attributes.getValue("name"));
576: actualRule.setMessage(attributes.getValue("message"));
577: actualRule.setReference(attributes.getValue("ref"));
578:
579: rules.add(actualRule);
580: }
581: }
582:
583: @Override
584: public void endElement(String namespaceURI, String sName, // simple name
585: String qName // qualified name
586: ) throws SAXException {
587: if (!isInRule) {
588: if (qName.equalsIgnoreCase("description")) {
589: rulesetDescription = lastChars.toString().trim();
590: lastChars.setLength(0);
591: }
592: /* else
593: {
594: //System.out.println("end1? "+qName);
595: }*/
596: } else {
597: if (qName.equalsIgnoreCase("description")) {
598: actualRule.setDescription(lastChars.toString()
599: .trim());
600: lastChars.setLength(0);
601: } else if (qName.equalsIgnoreCase("priority")) {
602: actualRule.setPriority(lastChars.toString().trim());
603: lastChars.setLength(0);
604: }
605: /* else
606: {
607: //System.out.println("end2? "+qName);
608: }*/
609: }
610: }
611:
612: @Override
613: public void characters(char[] buf, int offset, int len)
614: throws SAXException {
615: if (lastChars.length() > 0)
616: lastChars.append(" ");
617: lastChars.append(new String(buf, offset, len).trim());
618: }
619: }
620: }
621:
622: /*
623: Mandatory arguments:
624: 1) A java source code filename or directory
625: 2) A report format
626: 3) A ruleset filename or a comma-delimited string of ruleset filenames
627:
628: For example:
629: c:\> java -jar pmd-4.0.jar c:\my\source\code html unusedcode
630:
631: Optional arguments that may be put before or after the mandatory arguments:
632: -debug: prints debugging information
633: -targetjdk: specifies a language version to target - 1.3, 1.4, 1.5 or 1.6; default is 1.5
634: -cpus: specifies the number of threads to create
635: -encoding: specifies the character set encoding of the source code files PMD is reading (i.e., UTF-8)
636: -excludemarker: specifies the String that marks the a line which PMD should ignore; default is NOPMD
637: -shortnames: prints shortened filenames in the report
638: -linkprefix: path to HTML source, for summary html renderer only
639: -lineprefix: custom anchor to affected line in the source file, for summary html renderer only
640: -minimumpriority: rule priority threshold; rules with lower priority than they will not be used
641: -nojava: do not check Java files; default to check Java files
642: -jsp: check JSP/JSF files; default to do not check JSP/JSF files
643: -reportfile: send report output to a file; default to System.out
644: -benchmark: output a benchmark report upon completion; default to System.err
645:
646: For example:
647: c:\> java -jar pmd-4.0.jar c:\my\source\code text unusedcode,imports -targetjdk 1.5 -debug
648: c:\> java -jar pmd-4.0.jar c:\my\source\code xml basic,design -encoding UTF-8
649: */
|