package tide.exttools.PMD;
import tide.sources.SourceFile;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import tide.editor.linemessages.*;
import tide.editor.*;
import tide.project.*;
import tide.utils.*;
import snow.utils.gui.*;
import snow.utils.storage.*;
import snow.utils.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.xml.transform.stream.StreamSource;
/** PMD is nice and quick.
*/
public final class PMDLauncher// implements ChangedFilesSaveListener
{
private static PMDLauncher instance = null;
private PMDLauncher()
{
}
public static PMDLauncher getInstance()
{
if(instance==null) instance = new PMDLauncher();
return instance;
}
/** Should be called at startup and each project settings changes and load ops
*
public void configureAutoScan()
{
ProjectSettings settings = MainEditorFrame.instance.getActualProject();
boolean isAuto = settings.getBooleanProperty("pmd_autoApplyOnChanges",false) && PMDSettingsDialog.isConfigured();
if(isAuto)
{
// added only once !
MainEditorFrame.instance.editorPanel.addChangedFilesSaveListener(this);
}
else
{
MainEditorFrame.instance.editorPanel.removeChangedFilesSaveListener(this);
}
}*/
/** ChangedFilesSaveListener interface.
*/
public void filesSaved(final Set<SourceFile> sources)
{
if(!PMDSettingsDialog.isConfigured()) return;
// Enforce EDT, because we are not necessary beeing called from edt here
final ProgressModalDialog[] pmd = new ProgressModalDialog[1];
SwingSafeRunnable.invokeAndWait(new Runnable() { public void run() { // MUST WAIT HERE!
pmd[0] = new ProgressModalDialog(MainEditorFrame.instance,
"PMD autoanalysis ("+sources.size()+" file"+(sources.size()==1?"":"s")+")", false);
pmd[0].startDelayed(30000L); // big delay, normally never visible...
}});
Thread t = new Thread()
{
public void run()
{
try
{
_analyse(true, pmd[0], sources.toArray(new SourceFile[sources.size()])); // Silent
/*for(SourceFile sf : sources)
{
if(pmd.getWasCancelled()) break;
_analyse(sf, true, pmd); // Silent
}*/
} catch(Exception ex)
{
ex.printStackTrace();
}
finally
{
pmd[0].closeDialog();
}
}
};
t.start();
}
public static void _analyse(boolean silentMode, ProgressModalDialog pmd, SourceFile... sfs) throws Exception
{
if(sfs.length==1)
{
SourceFile sf = sfs[0];
if(sf.isSourceFile())
{
LineMessagesManager.getInstance().removeMessagesFor(sf.getJavaName(), PMDMessage.class );
analyse(silentMode, pmd, Arrays.asList(sf.javaFile));
}
else if(sf.isDirectory())
{
String pn = sf.getPackageName();
if(pn.length()>0) pn+=".";
LineMessagesManager.getInstance().removeMessagesForJavaNameStartingWith(pn, PMDMessage.class);
File d = new File(MainEditorFrame.instance.getActualProject().getSources_Home(),
sf.getJavaName().replace(".", "/"));
analyse( silentMode, pmd, Arrays.asList(d) );
}
}
else
{
List<File> lf = new ArrayList<File>();
for(SourceFile sfi : sfs)
{
LineMessagesManager.getInstance().removeMessagesFor(sfi.getJavaName(), PMDMessage.class );
lf.add(sfi.javaFile);
}
analyse(silentMode, pmd, lf);
}
}
/** As argument: either a source or a directory.
* must be called in a separate thread
*/
private static void analyse(boolean silentMode, ProgressModalDialog pmd, List<File> sourcesToAnalyse) throws Exception
{
ProjectSettings actualProject = MainEditorFrame.instance.getActualProject();
boolean ignoreIrrelevant = actualProject.getBooleanProperty("pmd_ignoreIrrelevant", false);
File pmdJar = new File(actualProject.getProperty("PMD_path", PMDSettingsDialog.defaultPMDLocation));
if(!pmdJar.exists()) throw new Exception ("pmd jar not found at "+pmdJar.getAbsolutePath());
File javaExePath = actualProject.getJava_TOOL();
String options = actualProject.getProperty("PMD_Options", PMDSettingsDialog.defaultOptions);
List<String> execCommand = new ArrayList<String>();
execCommand.add( javaExePath.getAbsolutePath() );
execCommand.add( "-jar" );
execCommand.add( pmdJar.getAbsolutePath() );
// 1: source (file or dir (recurse) )
if(sourcesToAnalyse.size()==1)
{
execCommand.add( sourcesToAnalyse.get(0).getAbsolutePath() );
}
else
{
// comma "," separated (as saw in sourcecode of PMD)
StringBuilder sb = new StringBuilder();
for(int i=0; i<sourcesToAnalyse.size()-1; i++)
{
sb.append( sourcesToAnalyse.get(i).getAbsolutePath());
sb.append( ",");
}
sb.append( sourcesToAnalyse.get(sourcesToAnalyse.size()-1).getAbsolutePath());
execCommand.add( sb.toString() );
}
// 2: report format
execCommand.add( "xml"); // html, xml
// 3: rules list
String rules = actualProject.getProperty("PMD_UsedRules", PMDSettingsDialog.recommandedPMDRules).trim();
rules = rules.replace(" ",","); // remove spaces
rules = rules.replace("\n",","); // and cr lf
rules = rules.replace("\r",""); //
rules = rules.replace(",,",","); // remove double ,
if(rules.toLowerCase().indexOf(".xml/")<0)
{
// OLD Style: nothing special specified...
execCommand.add( rules );
}
else
{
// we have "subrules" => create
File tf = new File(actualProject.getProjectSettingsFolder(), "pmdcustom.xml");
tf.deleteOnExit();
FileUtils.saveToFile( generateXMLCustomRuleSet(rules), tf);
execCommand.add( tf.getAbsolutePath() );
}
// 4: opts
if(options!=null && options.trim().length()>0)
{
execCommand.addAll( ProjectUtils.splitArgs(options,false));
}
if(!silentMode)
{
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.clearDocument();
MainEditorFrame.instance.outputPanels.selectToolsTab(false);
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendDatedLine("PMD analysis of "+sourcesToAnalyse+":\r\n");
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine("Used rules: "+rules);
}
MainEditorFrame.debugOut("Running PMD: "+execCommand);
Process proc = new ProcessBuilder(execCommand).start();
pmd.setProcessToOptionalKill(proc);
if(!silentMode) pmd.start();
if(!silentMode)
{
MainEditorFrame.instance.outputPanels.processesManager.addProcess("PMD analysis of "+sourcesToAnalyse+"", proc, true);
}
// write the analysis result in a file
File file = File.createTempFile("tIDE_pmd_analysis.tmp", ".xml"); //"temp/pmd_temp_res.xml");
file.deleteOnExit();
FileOutputStream fos = new FileOutputStream(file);
StreamGobbler sg = new StreamGobbler(proc.getInputStream(), fos);
//MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.createWriterForThisDocument(false), "");
sg.start();
// errors
StreamGobbler sge = new StreamGobbler(proc.getErrorStream(),
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.createWriterForThisDocument(true), "PMD");
sge.start();
// wait until proc completed...
proc.waitFor();
fos.close();
// XSLT
String xsltRelPath = "tide/exttools/PMD/simple_pmd_report.xsl";
InputStream is = PMDLauncher.class.getClassLoader().getResourceAsStream(xsltRelPath); // jar mode
if(is==null)
{
// IDE mode (develop)
//System.out.println("no PMD xsl found in jar");
File fi = new File(xsltRelPath);
if(fi.exists())
{
is = new FileInputStream( fi );
}
else
{
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendError("\nXSLT transform file not found in "+xsltRelPath);
System.out.println("File not found: "+fi);
}
}
if(is!=null)
{
try
{
String res = XMLUtils.transformXML( is, new StreamSource(file));
Map[] stats = parseMessages(res, ignoreIrrelevant);
if(!silentMode)
{
@SuppressWarnings("unchecked")
Map<String, Integer> catCounts = (Map<String, Integer>) stats[0];
@SuppressWarnings("unchecked")
Map<Integer, Integer> priorCounts = (Map<Integer, Integer>)stats[1];
int tot = 0;
for(Integer ci : catCounts.values())
{
tot += ci;
}
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine("\n"+tot+" messages generated (see Messages tab)");
// Stats print
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine("\n"+priorCounts.size()+" priorit"+(priorCounts.size()==1?"y":"ies")+":");
ArrayList<Integer> prior = new ArrayList<Integer>(priorCounts.keySet());
Collections.sort(prior);
for(Integer pi : prior)
{
int pic = priorCounts.get(pi);
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine("\tpriority "+pi+": \t"+pic+" message"+(pic==1?"":"s"));
}
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine("\n"+catCounts.size()+" categor"+(catCounts.size()==1?"y":"ies")+":");
ArrayList<String> cats = new ArrayList<String>(catCounts.keySet());
Collections.sort(cats);
for(String cai : cats)
{
int ci = catCounts.get(cai);
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine("\t"+ci+"\t "+cai);
}
}
}
catch(Exception e)
{
// occurs often during auto analysis, when the file is not "compilable" and has been saved...
// ex: javax.xml.transform.TransformerException: com.sun.org.apache.xml.internal.utils.WrappedRuntimeException: Premature end of file.
if(!silentMode)
{
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendError("ERROR during PMD XSLT transform: "+e.getMessage()+"\n");
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine(FileUtils.getFileStringContent(file));
//e.printStackTrace();
}
}
finally
{
is.close();
}
}
else
{
MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc.appendLine(FileUtils.getFileStringContent(file));
}
if(!silentMode)
{
MainEditorFrame.instance.outputPanels.toolsOutputPanel.setCaret0();
}
LineMessagesManager.getInstance().refreshView();
}
/** The formatted output is generated from the transformed pmd xml output.
* the format is some white line separated messages ending with the source position.
*/
private static Map[] parseMessages(String formattedOutput, boolean ignoreIrrelevant) throws Exception
{
Map<String, Integer> catCountStat = new HashMap<String, Integer>();
Map<Integer, Integer> priorityCount = new HashMap<Integer, Integer>();
String pa = MainEditorFrame.instance.getActualProject().getSources_Home().getAbsolutePath();
if(!pa.endsWith("\\")) pa+= "\\";
BufferedReader sr = new BufferedReader( new StringReader(formattedOutput));
StringBuilder actualMess = new StringBuilder();
String line = null;
int count = 0;
wl:while((line=sr.readLine())!=null)
{
//System.out.println(""+line);
if(line.startsWith("===")) // separate sources
{
actualMess.setLength(0);
continue wl;
}
actualMess.append("\n"+line);
if(line.startsWith(" at "))
{
String javaName = line.substring(4+pa.length());
int pos = javaName.indexOf(".java:");
int lineNr = -1;
int colNr = -1;
if(pos>0)
{
String[] lns = javaName.substring(pos+6).split(":");
javaName = javaName.substring(0,pos);
if(lns!=null && lns.length>0 && lns[0].length()>0)
{
lineNr = Integer.parseInt(lns[0]);
if(lns.length>1)
{
colNr = Integer.parseInt(lns[1])-1;
}
}
}
javaName = javaName.replace("\\", ".");
PMDMessage pmm = PMDMessage.createAndAdd(javaName, actualMess.toString().trim(), lineNr,colNr, ignoreIrrelevant);
count++;
actualMess.setLength(0);
String cat = pmm.getCategory();
if(LineMessagesManager.getInstance().getIrrelevantCategories().contains(cat))
{
// TODO: also include a stat of ignored...
continue;
}
// stats.
if(!catCountStat.containsKey(cat)) { catCountStat.put(cat,1);}
else {catCountStat.put( cat, catCountStat.get(cat)+1);}
int prior = pmm.getPriority();
if(!priorityCount.containsKey(prior)) { priorityCount.put(prior, 1);}
else {priorityCount.put( prior, priorityCount.get(prior)+1);}
}
}
return new Map[]{catCountStat, priorityCount};
}
public static String generateXMLCustomRuleSet(String rulesList)
{
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\"?>");
sb.append("\n<ruleset name=\"tIDE custom PMD ruleset\"");
sb.append("\nxmlns=\"http://pmd.sf.net/ruleset/1.0.0\"");
sb.append("\nxmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
sb.append("\nxsi:schemaLocation=\"http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd\"");
sb.append("\nxsi:noNamespaceSchemaLocation=\"http://pmd.sf.net/ruleset_xml_schema.xsd\">");
sb.append("\n\n<description>");
sb.append("\nThis ruleset contains the PMD rules used by tIDE");
sb.append("\n</description>\n");
for(String ri : rulesList.split(","))
{
sb.append("\n<rule ref=\""+ri.trim()+"\"/>");
}
sb.append("\n</ruleset>");
return sb.toString();
}
/** read all rules from jar.
* in the rulesets/ directory
*/
public static RuleSet readRulesetsNames(File pmdJarFile, boolean withrules) throws Exception
{
ArrayList<String> rs = new ArrayList<String>();
JarFile jf = null;
RuleSet root = new RuleSet("","","");
try
{
jf = new JarFile(pmdJarFile);
Enumeration<JarEntry> entries = jf.entries();
while(entries.hasMoreElements())
{
JarEntry je = entries.nextElement();
if(je.getName().startsWith("rulesets/"))
{
String en = je.getName();
if(en.toLowerCase().endsWith(".xml"))
{
rs.add(en);
if(withrules)
{
String cont = FileUtils.getStringContent( jf.getInputStream(je));
RuleSet ruse = parseRules(en, cont);
root.add(ruse);
}
}
}
}
}
catch(Exception e)
{
throw e;
}
finally
{
jf.close();
}
return root;
}
static int nn=0;
// minor bug: 38 != rulesets/releases/39.xml is named 38
private static RuleSet parseRules(String fullName, String cont)
{
// Use the default (non-validating) parser
SAXParserFactory factory = SAXParserFactory.newInstance();
// Parse the file
try
{
SAXParser saxParser = factory.newSAXParser();
InputSource is = new InputSource(new StringReader(cont));
RulesetParser rp = new RulesetParser();
saxParser.parse( is, rp);
RuleSet rs = new RuleSet(fullName, rp.rulesetName, rp.rulesetDescription);
// verification:
/*
int pos = fullName.lastIndexOf('/');
String name = fullName.substring(pos+1, fullName.length()-4);
if(!name.equals(rp.rulesetName))
{
//System.out.println("Ruleset name mismatch xml file name: "+rp.rulesetName+" != "+fullName);
}*/
for(Rule r : rp.rules)
{
rs.add(r);
}
return rs;
}
catch(Exception ex)
{
ex.printStackTrace();
System.out.println("Cannot parse <"+cont+">");
throw new RuntimeException("cnp");
}
}
/* public static void main(String[] aa)
{
PMDSettingsDialog.main(aa);
}*/
static class RulesetParser extends DefaultHandler
{
List<Rule> rules = new ArrayList<Rule>();
String rulesetName;
String rulesetDescription;
boolean isInRule = false;
StringBuilder lastChars = new StringBuilder();
Rule actualRule = null;
@Override
public void startDocument()
{
}
@Override
public void endDocument()
{
}
void pa(Attributes at)
{
for(int i=0; i<at.getLength(); i++)
{
System.out.print(""+at.getLocalName(i)+": "+at.getValue(i)+", ");
}
System.out.println("");
}
@Override
public void startElement(String namespaceURI, String sName, String qName, Attributes attributes)
throws SAXException
{
// reset
lastChars.setLength(0);
//sn is empty.
//pa(attributes);
if(qName.equalsIgnoreCase("ruleset"))
{
rulesetName = attributes.getValue("name");
}
else if(qName.equalsIgnoreCase("rule"))
{
isInRule = true;
actualRule = new Rule();
actualRule.setName( attributes.getValue("name") );
actualRule.setMessage( attributes.getValue("message") );
actualRule.setReference( attributes.getValue("ref") );
rules.add(actualRule);
}
}
@Override
public void endElement(String namespaceURI,
String sName, // simple name
String qName // qualified name
) throws SAXException
{
if(!isInRule)
{
if(qName.equalsIgnoreCase("description"))
{
rulesetDescription = lastChars.toString().trim();
lastChars.setLength(0);
}
/* else
{
//System.out.println("end1? "+qName);
}*/
}
else
{
if(qName.equalsIgnoreCase("description"))
{
actualRule.setDescription(lastChars.toString().trim());
lastChars.setLength(0);
}
else if(qName.equalsIgnoreCase("priority"))
{
actualRule.setPriority(lastChars.toString().trim());
lastChars.setLength(0);
}
/* else
{
//System.out.println("end2? "+qName);
}*/
}
}
@Override
public void characters(char[] buf, int offset, int len) throws SAXException
{
if(lastChars.length()>0) lastChars.append(" ");
lastChars.append( new String(buf,offset,len).trim() );
}
}
}
/*
Mandatory arguments:
1) A java source code filename or directory
2) A report format
3) A ruleset filename or a comma-delimited string of ruleset filenames
For example:
c:\> java -jar pmd-4.0.jar c:\my\source\code html unusedcode
Optional arguments that may be put before or after the mandatory arguments:
-debug: prints debugging information
-targetjdk: specifies a language version to target - 1.3, 1.4, 1.5 or 1.6; default is 1.5
-cpus: specifies the number of threads to create
-encoding: specifies the character set encoding of the source code files PMD is reading (i.e., UTF-8)
-excludemarker: specifies the String that marks the a line which PMD should ignore; default is NOPMD
-shortnames: prints shortened filenames in the report
-linkprefix: path to HTML source, for summary html renderer only
-lineprefix: custom anchor to affected line in the source file, for summary html renderer only
-minimumpriority: rule priority threshold; rules with lower priority than they will not be used
-nojava: do not check Java files; default to check Java files
-jsp: check JSP/JSF files; default to do not check JSP/JSF files
-reportfile: send report output to a file; default to System.out
-benchmark: output a benchmark report upon completion; default to System.err
For example:
c:\> java -jar pmd-4.0.jar c:\my\source\code text unusedcode,imports -targetjdk 1.5 -debug
c:\> java -jar pmd-4.0.jar c:\my\source\code xml basic,design -encoding UTF-8
*/
|