Browse Source

- Added rulespath to Audit to use customized rules.

- Fix Audit messages to remove fullpath in it. It
won't work on references though.
- Fix MAudit handler to print stderr as well so
that a missing agent or invalid options return
a message.
- Enhance options to reflect those available also
in Webgain QA 2.1.2+


git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@271226 13f79535-47bb-0310-9956-ffa450edef68
master
Stephane Bailliez 23 years ago
parent
commit
8af1905a98
5 changed files with 470 additions and 323 deletions
  1. +90
    -60
      src/main/org/apache/tools/ant/taskdefs/optional/metamata/AbstractMetamataTask.java
  2. +108
    -56
      src/main/org/apache/tools/ant/taskdefs/optional/metamata/MAudit.java
  3. +115
    -69
      src/main/org/apache/tools/ant/taskdefs/optional/metamata/MAuditStreamHandler.java
  4. +62
    -56
      src/main/org/apache/tools/ant/taskdefs/optional/metamata/MMetrics.java
  5. +95
    -82
      src/main/org/apache/tools/ant/taskdefs/optional/metamata/MMetricsStreamHandler.java

+ 90
- 60
src/main/org/apache/tools/ant/taskdefs/optional/metamata/AbstractMetamataTask.java View File

@@ -1,7 +1,7 @@
/*
* The Apache Software License, Version 1.1
*
* Copyright (c) 2001 The Apache Software Foundation. All rights
* Copyright (c) 2001-2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -53,25 +53,25 @@
*/
package org.apache.tools.ant.taskdefs.optional.metamata;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.DirectoryScanner;

import java.io.File;
import java.io.IOException;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Enumeration;
import java.util.Random;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.util.FileUtils;

/**
* Somewhat abstract framework to be used for other metama 2.0 tasks.
@@ -80,11 +80,9 @@ import java.util.Random;
* For more information, visit the website at
* <a href="http://www.metamata.com">www.metamata.com</a>
*
* @author <a href="mailto:sbailliez@imediation.com">Stephane Bailliez</a>
* @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a>
*/
public abstract class AbstractMetamataTask extends Task{

//--------------------------- ATTRIBUTES -----------------------------------
public abstract class AbstractMetamataTask extends Task {

/**
* The user classpath to be provided. It matches the -classpath of the
@@ -116,7 +114,7 @@ public abstract class AbstractMetamataTask extends Task{
// be set when calling scanFileSets();
protected Hashtable includedFiles = null;

public AbstractMetamataTask(){
public AbstractMetamataTask() {
}

/** initialize the task with the classname of the task to run */
@@ -126,8 +124,12 @@ public abstract class AbstractMetamataTask extends Task{
}

/** the metamata.home property to run all tasks. */
public void setMetamatahome(final File metamataHome){
this.metamataHome = metamataHome;
public void setHome(final File value) {
this.metamataHome = value;
}

public void setMetamatahome(final File value) {
setHome(value);
}

/** user classpath */
@@ -139,8 +141,8 @@ public abstract class AbstractMetamataTask extends Task{
}

/** create the source path for this task */
public Path createSourcepath(){
if (sourcePath == null){
public Path createSourcepath() {
if (sourcePath == null) {
sourcePath = new Path(project);
}
return sourcePath;
@@ -152,7 +154,7 @@ public abstract class AbstractMetamataTask extends Task{
}

/** -mx or -Xmx depending on VM version */
public void setMaxmemory(String max){
public void setMaxmemory(String max) {
if (Project.getJavaVersion().startsWith("1.1")) {
createJvmarg().setValue("-mx" + max);
} else {
@@ -177,8 +179,6 @@ public abstract class AbstractMetamataTask extends Task{
}
}

//--------------------- PRIVATE/PROTECTED METHODS --------------------------

/** check the options and build the command line */
protected void setUp() throws BuildException {
checkOptions();
@@ -190,10 +190,12 @@ public abstract class AbstractMetamataTask extends Task{

// set the metamata.home property
final Commandline.Argument vmArgs = cmdl.createVmArgument();
vmArgs.setValue("-Dmetamata.home=" + metamataHome.getAbsolutePath() );
vmArgs.setValue("-Dmetamata.home=" + metamataHome.getAbsolutePath());

// retrieve all the files we want to scan
includedFiles = scanFileSets();
includedFiles = scanSources(new Hashtable());
//String[] entries = sourcePath.list();
//includedFiles = scanSources(new Hashtable(), entries);
log(includedFiles.size() + " files added for audit", Project.MSG_VERBOSE);

// write all the options to a temp file and use it ro run the process
@@ -220,34 +222,33 @@ public abstract class AbstractMetamataTask extends Task{
if (process.execute() != 0) {
throw new BuildException("Metamata task failed.");
}
} catch (IOException e){
throw new BuildException("Failed to launch Metamata task: " + e);
} catch (IOException e) {
throw new BuildException("Failed to launch Metamata task", e);
}
}

/** clean up all the mess that we did with temporary objects */
protected void cleanUp(){
if (optionsFile != null){
protected void cleanUp() {
if (optionsFile != null) {
optionsFile.delete();
optionsFile = null;
}
}

/** return the location of the jar file used to run */
protected final File getMetamataJar(File home){
return new File(new File(home.getAbsolutePath()), "lib/metamata.jar");
protected final File getMetamataJar(File home) {
return new File(home, "lib/metamata.jar");
}

/** validate options set */
protected void checkOptions() throws BuildException {
// do some validation first
if (metamataHome == null || !metamataHome.exists()){
throw new BuildException("'metamatahome' must point to Metamata home directory.");
if (metamataHome == null || !metamataHome.exists()) {
throw new BuildException("'home' must point to Metamata home directory.");
}
metamataHome = project.resolveFile(metamataHome.getPath());
File jar = getMetamataJar(metamataHome);
if (!jar.exists()){
throw new BuildException( jar + " does not exist. Check your metamata installation.");
if (!jar.exists()) {
throw new BuildException(jar + " does not exist. Check your metamata installation.");
}
}

@@ -261,65 +262,94 @@ public abstract class AbstractMetamataTask extends Task{
fw = new FileWriter(tofile);
PrintWriter pw = new PrintWriter(fw);
final int size = options.size();
for (int i = 0; i < size; i++){
pw.println( options.elementAt(i) );
for (int i = 0; i < size; i++) {
pw.println(options.elementAt(i));
}
pw.flush();
} catch (IOException e){
} catch (IOException e) {
throw new BuildException("Error while writing options file " + tofile, e);
} finally {
if (fw != null){
if (fw != null) {
try {
fw.close();
} catch (IOException ignored){}
} catch (IOException ignored) {
}
}
}
}


protected Hashtable getFileMapping(){
protected Hashtable getFileMapping() {
return includedFiles;
}

/**
* convenient method for JDK 1.1. Will copy all elements from src to dest
*/
protected static final void addAllVector(Vector dest, Enumeration files){
protected static final void addAllVector(Vector dest, Enumeration files) {
while (files.hasMoreElements()) {
dest.addElement( files.nextElement() );
dest.addElement(files.nextElement());
}
}
protected static final File createTmpFile(){
// must be compatible with JDK 1.1 !!!!
final long rand = (new Random(System.currentTimeMillis())).nextLong();
File file = new File("metamata" + rand + ".tmp");
return file;

protected final File createTmpFile() {
return FileUtils.newFileUtils().createTempFile("metamata", ".tmp", getProject().getBaseDir());
}

/**
* @return the list of .java files (as their absolute path) that should
* be audited.
*/
protected Hashtable scanFileSets(){

protected Hashtable scanSources(Hashtable map) {
Hashtable files = new Hashtable();
for (int i = 0; i < fileSets.size(); i++){
for (int i = 0; i < fileSets.size(); i++) {
FileSet fs = (FileSet) fileSets.elementAt(i);
DirectoryScanner ds = fs.getDirectoryScanner(project);
ds.scan();
String[] f = ds.getIncludedFiles();
log(i + ") Adding " + f.length + " files from directory " + ds.getBasedir(), Project.MSG_VERBOSE);
for (int j = 0; j < f.length; j++){
for (int j = 0; j < f.length; j++) {
String pathname = f[j];
if ( pathname.endsWith(".java") ){
File file = new File( ds.getBasedir(), pathname);
if (pathname.endsWith(".java")) {
File file = new File(ds.getBasedir(), pathname);
// file = project.resolveFile(file.getAbsolutePath());
String classname = pathname.substring(0, pathname.length()-".java".length());
String classname = pathname.substring(0, pathname.length() - ".java".length());
classname = classname.replace(File.separatorChar, '.');
files.put( file.getAbsolutePath(), classname ); // it's a java file, add it.
files.put(file.getAbsolutePath(), classname); // it's a java file, add it.
}
}
}
return files;
}

protected Hashtable scanSources(final Hashtable mapping, final String[] entries) {
final Vector javaFiles = new Vector(512);
for (int i = 0; i < entries.length; i++) {
final File f = new File(entries[i]);
if (f.isDirectory()) {
DirectoryScanner ds = new DirectoryScanner();
ds.setBasedir(f);
ds.setIncludes(new String[]{"**/*.java"});
ds.scan();
String[] included = ds.getIncludedFiles();
for (int j = 0; j < included.length; j++) {
javaFiles.addElement(new File(f, included[j]));
}
} else if (entries[i].endsWith(".java")) {
javaFiles.addElement(f);
}
}
// do the mapping paths/classname
final int count = javaFiles.size();
for (int i = 0; i < count; i++) {
File file = (File) javaFiles.elementAt(i);
String pathname = Path.translateFile(file.getAbsolutePath());
String classname = pathname.substring(0, pathname.length() - ".java".length());
classname = classname.replace(File.separatorChar, '.');
mapping.put(pathname, classname);
}
return mapping;
}

}

+ 108
- 56
src/main/org/apache/tools/ant/taskdefs/optional/metamata/MAudit.java View File

@@ -1,7 +1,7 @@
/*
* The Apache Software License, Version 1.1
*
* Copyright (c) 2001 The Apache Software Foundation. All rights
* Copyright (c) 2001-2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -53,21 +53,19 @@
*/
package org.apache.tools.ant.taskdefs.optional.metamata;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;

import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;



import java.io.File;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Vector;

/**
* Metamata Audit evaluates Java code for programming errors, weaknesses, and
* style violation.
@@ -81,7 +79,7 @@ import java.util.Vector;
* For more information, visit the website at
* <a href="http://www.metamata.com">www.metamata.com</a>
*
* @author <a href="mailto:sbailliez@imediation.com">Stephane Bailliez</a>
* @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a>
*/
public class MAudit extends AbstractMetamataTask {

@@ -119,15 +117,24 @@ public class MAudit extends AbstractMetamataTask {
// (?:file:)?((?#filepath).+):((?#line)\\d+)\\s*:\\s+((?#message).*)
final static String AUDIT_PATTERN = "(?:file:)?(.+):(\\d+)\\s*:\\s+(.*)";

protected File outFile = null;
protected Path searchPath = null;
protected boolean fix = false;
protected boolean list = false;
protected boolean unused = false;
private File outFile = null;

private Path searchPath = null;

private Path rulesPath = null;

private boolean fix = false;

private boolean list = false;

private boolean unused = false;

// add a bunch of undocumented options for the task
private boolean quiet = false;
private boolean exit = false;
private boolean offsets = false;
private boolean verbose = false;
private boolean fullsemanticize = false;

/** default constructor */
public MAudit() {
@@ -135,49 +142,101 @@ public class MAudit extends AbstractMetamataTask {
}

/** set the destination file which should be an xml file */
public void setTofile(File outFile){
public void setTofile(File outFile) {
this.outFile = outFile;
}

public void setFix(boolean flag){
public void setFix(boolean flag) {
this.fix = flag;
}

public void setList(boolean flag){
public void setList(boolean flag) {
this.list = flag;
}

public void setUnused(boolean flag){
public void setUnused(boolean flag) {
this.unused = flag;
}

public Path createSearchpath(){
if (searchPath == null){
searchPath = new Path(project);
public void setQuiet(boolean flag) {
this.quiet = flag;
}

public void setExit(boolean flag) {
this.exit = flag;
}

public void setOffsets(boolean flag) {
this.offsets = flag;
}

public void setVerbose(boolean flag) {
this.verbose = flag;
}

public void setFullsemanticize(boolean flag) {
this.fullsemanticize = flag;
}

/** one or more path for rules that must be placed before metamata.jar !! */
public Path createRulespath() {
if (rulesPath == null) {
rulesPath = new Path(getProject());
}
return rulesPath;
}

/** search path to use for unused global declarations */
public Path createSearchpath() {
if (searchPath == null) {
searchPath = new Path(getProject());
}
return searchPath;
}

protected Vector getOptions(){
protected Vector getOptions() {
Vector options = new Vector(512);
// add the source path automatically from the fileset.
// to avoid redundancy...
for (int i = 0; i < fileSets.size(); i++) {
FileSet fs = (FileSet) fileSets.elementAt(i);
Path path = createSourcepath();
File dir = fs.getDir(getProject());
path.setLocation(dir);
}

// there is a bug in Metamata 2.0 build 37. The sourcepath argument does
// not work. So we will use the sourcepath prepended to classpath. (order
// is important since Metamata looks at .class and .java)
if (sourcePath != null){
if (sourcePath != null) {
sourcePath.append(classPath); // srcpath is prepended
classPath = sourcePath;
sourcePath = null; // prevent from using -sourcepath
}
}
// don't forget to modify the pattern if you change the options reporting
if (classPath != null){
if (classPath != null) {
options.addElement("-classpath");
options.addElement(classPath.toString());
}
// suppress copyright msg when running, we will let it so that this
// will be the only output to the console if in xml mode
// options.addElement("-quiet");
if (fix){
if (quiet) {
options.addElement("-quiet");
}
if (fullsemanticize) {
options.addElement("-full-semanticize");
}
if (verbose) {
options.addElement("-verbose");
}
if (offsets) {
options.addElement("-offsets");
}
if (exit) {
options.addElement("-exit");
}
if (fix) {
options.addElement("-fix");
}
options.addElement("-fullpath");
@@ -185,43 +244,45 @@ public class MAudit extends AbstractMetamataTask {
// generate .maudit files much more detailed than the report
// I don't like it very much, I think it could be interesting
// to get all .maudit files and include them in the XML.
if (list){
if (list) {
options.addElement("-list");
}
if (sourcePath != null){
if (sourcePath != null) {
options.addElement("-sourcepath");
options.addElement(sourcePath.toString());
}
if (unused){
addAllVector(options, includedFiles.keys());
if (unused) {
options.addElement("-unused");
options.addElement(searchPath.toString());
}
addAllVector(options, includedFiles.keys());
return options;
}

protected void checkOptions() throws BuildException {
super.checkOptions();
if (unused && searchPath == null){
if (unused && searchPath == null) {
throw new BuildException("'searchpath' element must be set when looking for 'unused' declarations.");
}
if (!unused && searchPath != null){
if (!unused && searchPath != null) {
log("'searchpath' element ignored. 'unused' attribute is disabled.", Project.MSG_WARN);
}
if (rulesPath != null) {
cmdl.createClasspath(getProject()).addExisting(rulesPath);
}
}
protected ExecuteStreamHandler createStreamHandler() throws BuildException {
ExecuteStreamHandler handler = null;
// if we didn't specify a file, then use a screen report
if (outFile == null){
handler = new LogStreamHandler(this, Project.MSG_INFO, Project.MSG_INFO);
if (outFile == null) {
handler = new LogStreamHandler(this, Project.MSG_INFO, Project.MSG_INFO);
} else {
try {
//XXX
OutputStream out = new FileOutputStream( outFile );
OutputStream out = new FileOutputStream(outFile);
handler = new MAuditStreamHandler(this, out);
} catch (IOException e){
} catch (IOException e) {
throw new BuildException(e);
}
}
@@ -242,18 +303,9 @@ public class MAudit extends AbstractMetamataTask {

/** the inner class used to report violation information */
final static class Violation {
int line;
String line;
String error;
}

/** handy factory to create a violation */
static final Violation createViolation(int line, String msg){
Violation violation = new Violation();
violation.line = line;
violation.error = msg;
return violation;
}

}



+ 115
- 69
src/main/org/apache/tools/ant/taskdefs/optional/metamata/MAuditStreamHandler.java View File

@@ -1,7 +1,7 @@
/*
* The Apache Software License, Version 1.1
*
* Copyright (c) 2001 The Apache Software Foundation. All rights
* Copyright (c) 2001-2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -53,31 +53,34 @@
*/
package org.apache.tools.ant.taskdefs.optional.metamata;

import org.apache.tools.ant.Project;

import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;

import org.apache.tools.ant.util.regexp.RegexpMatcher;
import org.apache.tools.ant.util.regexp.RegexpMatcherFactory;

import org.apache.tools.ant.util.DOMElementWriter;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.io.BufferedReader;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Writer;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Hashtable;
import java.io.Writer;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.taskdefs.LogOutputStream;
import org.apache.tools.ant.taskdefs.StreamPumper;
import org.apache.tools.ant.util.DOMElementWriter;
import org.apache.tools.ant.util.DateUtils;
import org.apache.tools.ant.util.StringUtils;
import org.apache.tools.ant.util.regexp.RegexpMatcher;
import org.apache.tools.ant.util.regexp.RegexpMatcherFactory;


/**
* This is a very bad stream handler for the MAudit task.
@@ -93,31 +96,41 @@ import javax.xml.parsers.DocumentBuilderFactory;
* <li>it does not report error that goes to stderr.
* </ul>
*
* @author <a href="sbailliez@imediation.com">Stephane Bailliez</a>
* @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a>
*/
class MAuditStreamHandler implements ExecuteStreamHandler {

protected MAudit task;
/** parent task */
private MAudit task;

/** reader for stdout */
protected BufferedReader br;
private BufferedReader br;

/** matcher that will be used to extract the info from the line */
protected RegexpMatcher matcher;
private RegexpMatcher matcher;

/**
* this is where the XML output will go, should mostly be a file
* the caller is responsible for flushing and closing this stream
*/
protected OutputStream xmlOut = null;
private OutputStream xmlOut = null;

/** error stream, might be useful to spit out error messages */
private OutputStream errStream;

/** thread pumping out error stream */
private Thread errThread;

/**
* the multimap. The key in the map is the filepath that caused the audit
* error and the value is a vector of MAudit.Violation entries.
*/
protected Hashtable auditedFiles = new Hashtable();
private Hashtable auditedFiles = new Hashtable();

MAuditStreamHandler(MAudit task, OutputStream xmlOut){
/** program start timestamp for reporting purpose */
private Date program_start;

MAuditStreamHandler(MAudit task, OutputStream xmlOut) {
this.task = task;
this.xmlOut = xmlOut;
/** the matcher should be the Oro one. I don't know about the other one */
@@ -126,10 +139,14 @@ class MAuditStreamHandler implements ExecuteStreamHandler {
}

/** Ignore. */
public void setProcessInputStream(OutputStream os) {}
public void setProcessInputStream(OutputStream os) {
}

/** Ignore. */
public void setProcessErrorStream(InputStream is) {}
public void setProcessErrorStream(InputStream is) {
errStream = new LogOutputStream(task, Project.MSG_ERR);
errThread = createPump(is, errStream);
}

/** Set the inputstream */
public void setProcessOutputStream(InputStream is) throws IOException {
@@ -138,6 +155,8 @@ class MAuditStreamHandler implements ExecuteStreamHandler {

/** Invokes parseOutput. This will block until the end :-(*/
public void start() throws IOException {
program_start = new Date();
errThread.start();
parseOutput(br);
}

@@ -146,19 +165,32 @@ class MAuditStreamHandler implements ExecuteStreamHandler {
* the MAudit output and write it to the output.
*/
public void stop() {
// make sure to flush err stream
try {
errThread.join();
} catch (InterruptedException e) {
}
try {
errStream.flush();
} catch (IOException e) {
}
// serialize the content as XML, move this to another method
// this is the only code that could be needed to be overrided
// this is the only code that could be needed to be overriden
Document doc = getDocumentBuilder().newDocument();
Element rootElement = doc.createElement("classes");
Enumeration keys = auditedFiles.keys();
Hashtable filemapping = task.getFileMapping();
final Date now = new Date();
rootElement.setAttribute("snapshot_created", DateUtils.format(now, DateUtils.ISO8601_DATETIME_PATTERN));
rootElement.setAttribute("elapsed_time", String.valueOf(now.getTime() - program_start.getTime()));
rootElement.setAttribute("program_start", DateUtils.format(now, DateUtils.ISO8601_DATETIME_PATTERN));
rootElement.setAttribute("audited", String.valueOf(filemapping.size()));
rootElement.setAttribute("reported", String.valueOf(auditedFiles.size()));
int errors = 0;
while (keys.hasMoreElements()){
String filepath = (String)keys.nextElement();
Vector v = (Vector)auditedFiles.get(filepath);
String fullclassname = (String)filemapping.get(filepath);
while (keys.hasMoreElements()) {
String filepath = (String) keys.nextElement();
Vector v = (Vector) auditedFiles.get(filepath);
String fullclassname = (String) filemapping.get(filepath);
if (fullclassname == null) {
task.getProject().log("Could not find class mapping for " + filepath, Project.MSG_WARN);
continue;
@@ -169,12 +201,13 @@ class MAuditStreamHandler implements ExecuteStreamHandler {
Element clazz = doc.createElement("class");
clazz.setAttribute("package", pkg);
clazz.setAttribute("name", clazzname);
clazz.setAttribute("violations", String.valueOf(v.size()));
errors += v.size();
for (int i = 0; i < v.size(); i++){
MAudit.Violation violation = (MAudit.Violation)v.elementAt(i);
final int violationCount = v.size();
clazz.setAttribute("violations", String.valueOf(violationCount));
errors += violationCount;
for (int i = 0; i < violationCount; i++) {
MAudit.Violation violation = (MAudit.Violation) v.elementAt(i);
Element error = doc.createElement("violation");
error.setAttribute("line", String.valueOf(violation.line));
error.setAttribute("line", violation.line);
error.setAttribute("message", violation.error);
clazz.appendChild(error);
}
@@ -183,54 +216,67 @@ class MAuditStreamHandler implements ExecuteStreamHandler {
rootElement.setAttribute("violations", String.valueOf(errors));

// now write it to the outputstream, not very nice code
if (xmlOut != null) {
Writer wri = null;
try {
wri = new OutputStreamWriter(xmlOut, "UTF-8");
wri.write("<?xml version=\"1.0\"?>\n");
(new DOMElementWriter()).write(rootElement, wri, 0, " ");
wri.flush();
} catch(IOException exc) {
task.log("Unable to write log file", Project.MSG_ERR);
} finally {
if (xmlOut != System.out && xmlOut != System.err) {
if (wri != null) {
try {
wri.close();
} catch (IOException e) {}
}
Writer wri = null;
try {
wri = new OutputStreamWriter(xmlOut, "UTF-8");
wri.write("<?xml version=\"1.0\"?>\n");
(new DOMElementWriter()).write(rootElement, wri, 0, " ");
wri.flush();
} catch (IOException exc) {
task.log("Unable to write log file", Project.MSG_ERR);
} finally {
if (wri != null) {
try {
wri.close();
} catch (IOException e) {
}
}
}

}

protected static DocumentBuilder getDocumentBuilder() {
try {
return DocumentBuilderFactory.newInstance().newDocumentBuilder();
}
catch(Exception exc) {
} catch (Exception exc) {
throw new ExceptionInInitializerError(exc);
}
}

/**
* Creates a stream pumper to copy the given input stream to the given output stream.
*/
protected Thread createPump(InputStream is, OutputStream os) {
final Thread result = new Thread(new StreamPumper(is, os));
result.setDaemon(true);
return result;
}


/** read each line and process it */
protected void parseOutput(BufferedReader br) throws IOException {
String line = null;
while ( (line = br.readLine()) != null ){
while ((line = br.readLine()) != null) {
processLine(line);
}
}

// we suppose here that there is only one report / line.
// There will obviouslly be a problem if the message is on several lines...
protected void processLine(String line){
protected void processLine(String line) {
Vector matches = matcher.getGroups(line);
if (matches != null) {
String file = (String)matches.elementAt(1);
int lineNum = Integer.parseInt((String)matches.elementAt(2));
String msg = (String)matches.elementAt(3);
addViolationEntry(file, MAudit.createViolation(lineNum, msg) );
String file = (String) matches.elementAt(1);
MAudit.Violation violation = new MAudit.Violation();
violation.line = (String) matches.elementAt(2);
violation.error = (String) matches.elementAt(3);
// remove the pathname from any messages and let the classname only.
final int pos = file.lastIndexOf(File.separatorChar);
if ((pos != -1) && (pos != file.length() - 1)) {
String filename = file.substring(pos + 1);
violation.error = StringUtils.replace(violation.error,
"file:" + file, filename);
}
addViolationEntry(file, violation);
} else {
// this doesn't match..report it as info, it could be
// either the copyright, summary or a multiline message (damn !)
@@ -239,14 +285,14 @@ class MAuditStreamHandler implements ExecuteStreamHandler {
}

/** add a violation entry for the file */
protected void addViolationEntry(String file, MAudit.Violation entry){
Vector violations = (Vector)auditedFiles.get(file);
// if there is no decl for this file yet, create it.
if (violations == null){
violations = new Vector();
auditedFiles.put(file, violations);
}
violations.add( entry );
protected void addViolationEntry(String file, MAudit.Violation entry) {
Vector violations = (Vector) auditedFiles.get(file);
// if there is no decl for this file yet, create it.
if (violations == null) {
violations = new Vector();
auditedFiles.put(file, violations);
}
violations.addElement(entry);
}

}

+ 62
- 56
src/main/org/apache/tools/ant/taskdefs/optional/metamata/MMetrics.java View File

@@ -53,21 +53,19 @@
*/
package org.apache.tools.ant.taskdefs.optional.metamata;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;

import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.Path;



import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileOutputStream;
import java.util.Vector;

/**
* Calculates global complexity and quality metrics on Java source code.
*
@@ -114,15 +112,15 @@ Format Options
*/

/** the granularity mode. Should be one of 'files', 'methods' and 'types'. */
protected String granularity = null;
private String granularity = null;

/** the XML output file */
protected File outFile = null;
private File outFile = null;
/** the location of the temporary txt report */
protected File tmpFile = createTmpFile();
private File tmpFile;

protected Path path = null;
private Path path = null;

//--------------------------- PUBLIC METHODS -------------------------------

@@ -131,20 +129,29 @@ Format Options
super("com.metamata.sc.MMetrics");
}

/**
* Attributes for granularity.
*/
public static class GranularityAttribute extends EnumeratedAttribute {
public String[] getValues() {
return new String[]{"compilation-units", "files", "methods", "types", "packages"};
}
}

/**
* set the granularity of the audit. Should be one of 'files', 'methods'
* or 'types'.
* @param granularity the audit reporting mode.
*/
public void setGranularity(String granularity){
this.granularity = granularity;
public void setGranularity(GranularityAttribute granularity) {
this.granularity = granularity.getValue();
}

/**
* Set the output XML file
* @param file the xml file to write the XML report to.
*/
public void setTofile(File file){
public void setTofile(File file) {
this.outFile = file;
}

@@ -152,7 +159,7 @@ Format Options
* Set a new path (directory) to measure metrics from.
* @return the path instance to use.
*/
public Path createPath(){
public Path createPath() {
if (path == null) {
path = new Path(project);
}
@@ -167,27 +174,24 @@ Format Options
protected void checkOptions() throws BuildException {
super.checkOptions();

if ( !"files".equals(granularity) && !"methods".equals(granularity)
&& !"types".equals(granularity) ){
throw new BuildException("Metrics reporting granularity is invalid. Must be one of 'files', 'methods', 'types'");
}
if (outFile == null){
if (outFile == null) {
throw new BuildException("Output XML file must be set via 'tofile' attribute.");
}
if (path == null && fileSets.size() == 0){
if (path == null && fileSets.size() == 0) {
throw new BuildException("Must set either paths (path element) or files (fileset element)");
}
// I don't accept dirs and files at the same time, I cannot recognize the semantic in the result
if (path != null && fileSets.size() > 0){
if (path != null && fileSets.size() > 0) {
throw new BuildException("Cannot set paths (path element) and files (fileset element) at the same time");
}
tmpFile = createTmpFile();
}

protected void execute0(ExecuteStreamHandler handler) throws BuildException {
super.execute0(handler);
transformFile();
}
/**
* transform the generated file via the handler
* This function can either be called if the result is written to the output
@@ -197,8 +201,8 @@ Format Options
protected void transformFile() throws BuildException {
FileInputStream tmpStream = null;
try {
tmpStream = new FileInputStream( tmpFile );
} catch (IOException e){
tmpStream = new FileInputStream(tmpFile);
} catch (IOException e) {
throw new BuildException("Error reading temporary file: " + tmpFile, e);
}
FileOutputStream xmlStream = null;
@@ -208,29 +212,31 @@ Format Options
xmlHandler.setProcessOutputStream(tmpStream);
xmlHandler.start();
xmlHandler.stop();
} catch (IOException e){
} catch (IOException e) {
throw new BuildException("Error creating output file: " + outFile, e);
} finally {
if (xmlStream != null){
if (xmlStream != null) {
try {
xmlStream.close();
} catch (IOException ignored){}
} catch (IOException ignored) {
}
}
if (tmpStream != null){
if (tmpStream != null) {
try {
tmpStream.close();
} catch (IOException ignored){}
} catch (IOException ignored) {
}
}
}
}

/** cleanup the temporary txt report */
protected void cleanUp() throws BuildException {
try {
super.cleanUp();
} finally {
if (tmpFile != null){
if (tmpFile != null) {
tmpFile.delete();
tmpFile = null;
}
@@ -242,52 +248,52 @@ Format Options
* a normal logger here, otherwise we could use the metrics handler
* directly to capture and transform the output on stdout to XML.
*/
protected ExecuteStreamHandler createStreamHandler(){
protected ExecuteStreamHandler createStreamHandler() {
// write the report directtly to an XML stream
// return new MMetricsStreamHandler(this, xmlStream);
return new LogStreamHandler(this, Project.MSG_INFO, Project.MSG_INFO);
}


protected Vector getOptions(){
protected Vector getOptions() {
Vector options = new Vector(512);
// there is a bug in Metamata 2.0 build 37. The sourcepath argument does
// not work. So we will use the sourcepath prepended to classpath. (order
// is important since Metamata looks at .class and .java)
if (sourcePath != null){
if (sourcePath != null) {
sourcePath.append(classPath); // srcpath is prepended
classPath = sourcePath;
sourcePath = null; // prevent from using -sourcepath
}
// don't forget to modify the pattern if you change the options reporting
if (classPath != null){
if (classPath != null) {
options.addElement("-classpath");
options.addElement(classPath);
options.addElement(classPath.toString());
}
options.addElement( "-output" );
options.addElement( tmpFile.toString() );
options.addElement( "-" + granularity);
options.addElement("-output");
options.addElement(tmpFile.toString());
options.addElement("-" + granularity);
// display the metamata copyright
// options.addElement( "-quiet");
options.addElement( "-format");
options.addElement("-format");
// need this because that's what the handler is using, it's
// way easier to process than any other separator
options.addElement( "tab");
options.addElement("tab");
// specify a / as the indent character, used by the handler.
options.addElement( "-i");
options.addElement( "/");
options.addElement("-i");
options.addElement("/");
// directories
String[] dirs = path.list();
for (int i = 0; i < dirs.length; i++){
options.addElement( dirs[i] );
for (int i = 0; i < dirs.length; i++) {
options.addElement(dirs[i]);
}
// files next.
// files next.
addAllVector(options, includedFiles.keys());
return options;
}


+ 95
- 82
src/main/org/apache/tools/ant/taskdefs/optional/metamata/MMetricsStreamHandler.java View File

@@ -54,32 +54,36 @@
package org.apache.tools.ant.taskdefs.optional.metamata;


import org.xml.sax.SAXException;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.sax.SAXTransformerFactory;
import java.util.Stack;
import java.util.EmptyStackException;
import java.util.Enumeration;
import java.util.Vector;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.EmptyStackException;
import java.util.Enumeration;
import java.util.Stack;
import java.util.Vector;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.Task;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.util.DateUtils;

/**
* A handy metrics handler. Most of this code was done only with the
@@ -95,44 +99,44 @@ import org.apache.tools.ant.Project;
public class MMetricsStreamHandler implements ExecuteStreamHandler {

/** CLASS construct, it should be named something like 'MyClass' */
protected final static String CLASS = "class";
private final static String CLASS = "class";

/** package construct, it should be look like 'com.mycompany.something' */
protected final static String PACKAGE = "package";
private final static String PACKAGE = "package";

/** FILE construct, it should look like something 'MyClass.java' or 'MyClass.class' */
protected final static String FILE = "file";
private final static String FILE = "file";

/** METHOD construct, it should looke like something 'doSomething(...)' or 'doSomething()' */
protected final static String METHOD = "method";
private final static String METHOD = "method";

protected final static String[] ATTRIBUTES = { "name", "vg", "loc",
"dit", "noa", "nrm", "nlm", "wmc", "rfc", "dac", "fanout", "cbo", "lcom", "nocl"
};
private final static String[] ATTRIBUTES = {
"name", "vg", "loc", "dit", "noa", "nrm", "nlm", "wmc",
"rfc", "dac", "fanout", "cbo", "lcom", "nocl"};

/** reader for stdout */
protected InputStream metricsOutput;
private InputStream metricsOutput;

/**
* this is where the XML output will go, should mostly be a file
* the caller is responsible for flushing and closing this stream
*/
protected OutputStream xmlOutputStream;
private OutputStream xmlOutputStream;

/** metrics handler */
protected TransformerHandler metricsHandler;
private TransformerHandler metricsHandler;

/** the task */
protected Task task;
private Task task;

/**
* the stack where are stored the metrics element so that they we can
* know if we have to close an element or not.
*/
protected Stack stack = new Stack();
private Stack stack = new Stack();

/** initialize this handler */
MMetricsStreamHandler(Task task, OutputStream xmlOut){
MMetricsStreamHandler(Task task, OutputStream xmlOut) {
this.task = task;
this.xmlOutputStream = xmlOut;
}
@@ -147,34 +151,39 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {

/** Set the inputstream */
public void setProcessOutputStream(InputStream is) throws IOException {
metricsOutput = is;
metricsOutput = is;
}

public void start() throws IOException {
// create the transformer handler that will be used to serialize
// the output.
TransformerFactory factory = TransformerFactory.newInstance();
if ( !factory.getFeature(SAXTransformerFactory.FEATURE) ){
if (!factory.getFeature(SAXTransformerFactory.FEATURE)) {
throw new IllegalStateException("Invalid Transformer factory feature");
}
try {
metricsHandler = ((SAXTransformerFactory)factory).newTransformerHandler();
metricsHandler.setResult( new StreamResult( new OutputStreamWriter(xmlOutputStream, "UTF-8")) );
metricsHandler = ((SAXTransformerFactory) factory).newTransformerHandler();
metricsHandler.setResult(new StreamResult(new OutputStreamWriter(xmlOutputStream, "UTF-8")));
Transformer transformer = metricsHandler.getTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");

// start the document with a 'metrics' root
final Date now = new Date();
metricsHandler.startDocument();
AttributesImpl attr = new AttributesImpl();
attr.addAttribute("", "company", "company", "CDATA", "metamata");
attr.addAttribute("", "snapshot_created", "snapshot_created", "CDATA",
DateUtils.format(now, DateUtils.ISO8601_DATETIME_PATTERN));
// attr.addAttribute("", "elapsed_time", "elapsed_time", "CDATA", String.valueOf(now.getTime() - program_start.getTime()));
attr.addAttribute("", "program_start", "program_start", "CDATA",
DateUtils.format(new Date(), DateUtils.ISO8601_DATETIME_PATTERN));
metricsHandler.startElement("", "metrics", "metrics", attr);

// now parse the whole thing
parseOutput();

} catch (Exception e){
e.printStackTrace();
throw new IOException(e.getMessage());
} catch (Exception e) {
throw new BuildException(e);
}
}

@@ -185,15 +194,15 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
try {
// we need to pop everything and close elements that have not been
// closed yet.
while ( stack.size() > 0){
ElementEntry elem = (ElementEntry)stack.pop();
while (stack.size() > 0) {
ElementEntry elem = (ElementEntry) stack.pop();
metricsHandler.endElement("", elem.getType(), elem.getType());
}
// close the root
metricsHandler.endElement("", "metrics", "metrics");
// document is finished for good
metricsHandler.endDocument();
} catch (SAXException e){
} catch (SAXException e) {
e.printStackTrace();
throw new IllegalStateException(e.getMessage());
}
@@ -203,7 +212,7 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
protected void parseOutput() throws IOException, SAXException {
BufferedReader br = new BufferedReader(new InputStreamReader(metricsOutput));
String line = null;
while ( (line = br.readLine()) != null ){
while ((line = br.readLine()) != null) {
processLine(line);
}
}
@@ -214,16 +223,16 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
* @param line the line to process, it is normally a line full of metrics.
*/
protected void processLine(String line) throws SAXException {
if ( line.startsWith("Construct\tV(G)\tLOC\tDIT\tNOA\tNRM\tNLM\tWMC\tRFC\tDAC\tFANOUT\tCBO\tLCOM\tNOCL") ){
if (line.startsWith("Construct\tV(G)\tLOC\tDIT\tNOA\tNRM\tNLM\tWMC\tRFC\tDAC\tFANOUT\tCBO\tLCOM\tNOCL")) {
return;
}
try {
MetricsElement elem = MetricsElement.parse(line);
startElement(elem);
} catch (ParseException e) {
e.printStackTrace();
//e.printStackTrace();
// invalid lines are sent to the output as information, it might be anything,
task.log(line, Project.MSG_INFO);
task.log(line, Project.MSG_INFO);
}
}

@@ -238,16 +247,17 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
// if there are elements in the stack we possibly need to close one or
// more elements previous to this one until we got its parent
int indent = elem.getIndent();
if ( stack.size() > 0 ){
ElementEntry previous = (ElementEntry)stack.peek();
if (stack.size() > 0) {
ElementEntry previous = (ElementEntry) stack.peek();
// close nodes until you got the parent.
try {
while ( indent <= previous.getIndent() && stack.size() > 0){
while (indent <= previous.getIndent() && stack.size() > 0) {
stack.pop();
metricsHandler.endElement("", previous.getType(), previous.getType());
previous = (ElementEntry)stack.peek();
previous = (ElementEntry) stack.peek();
}
} catch (EmptyStackException ignored){}
} catch (EmptyStackException ignored) {
}
}

// ok, now start the new construct
@@ -256,7 +266,7 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
metricsHandler.startElement("", type, type, attrs);

// make sure we keep track of what we did, that's history
stack.push( new ElementEntry(type, indent) );
stack.push(new ElementEntry(type, indent));
}

/**
@@ -267,36 +277,36 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
* @return the type of the metrics element, either PACKAGE, FILE, CLASS or
* METHOD.
*/
protected String getConstructType(MetricsElement elem){
protected String getConstructType(MetricsElement elem) {
// ok no doubt, it's a file
if ( elem.isCompilationUnit() ){
if (elem.isCompilationUnit()) {
return FILE;
}

// same, we're sure it's a method
if ( elem.isMethod() ){
if (elem.isMethod()) {
return METHOD;
}

// if it's empty, and none of the above it should be a package
if ( stack.size() == 0 ){
if (stack.size() == 0) {
return PACKAGE;
}

// ok, this is now black magic time, we will guess the type based on
// the previous type and its indent...
final ElementEntry previous = (ElementEntry)stack.peek();
final ElementEntry previous = (ElementEntry) stack.peek();
final String prevType = previous.getType();
final int prevIndent = previous.getIndent();
final int indent = elem.getIndent();
// we're just under a file with a bigger indent so it's a class
if ( prevType.equals(FILE) && indent > prevIndent ){
if (prevType.equals(FILE) && indent > prevIndent) {
return CLASS;
}

// we're just under a class with a greater or equals indent, it's a class
// (there might be several classes in a compilation unit and inner classes as well)
if ( prevType.equals(CLASS) && indent >= prevIndent ){
if (prevType.equals(CLASS) && indent >= prevIndent) {
return CLASS;
}

@@ -308,17 +318,16 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
/**
* Create all attributes of a MetricsElement skipping those who have an
* empty string
* @param elem
*/
protected Attributes createAttributes(MetricsElement elem){
protected Attributes createAttributes(MetricsElement elem) {
AttributesImpl impl = new AttributesImpl();
int i = 0;
String name = ATTRIBUTES[i++];
impl.addAttribute("", name, name, "CDATA", elem.getName());
Enumeration metrics = elem.getMetrics();
for (; metrics.hasMoreElements(); i++){
String value = (String)metrics.nextElement();
if ( value.length() > 0 ){
for (; metrics.hasMoreElements(); i++) {
String value = (String) metrics.nextElement();
if (value.length() > 0) {
name = ATTRIBUTES[i];
impl.addAttribute("", name, name, "CDATA", value);
}
@@ -333,13 +342,16 @@ public class MMetricsStreamHandler implements ExecuteStreamHandler {
private final static class ElementEntry {
private String type;
private int indent;
ElementEntry(String type, int indent){

ElementEntry(String type, int indent) {
this.type = type;
this.indent = indent;
}
public String getType(){

public String getType() {
return type;
}

public int getIndent() {
return indent;
}
@@ -351,6 +363,7 @@ class MetricsElement {
private final static NumberFormat METAMATA_NF;

private final static NumberFormat NEUTRAL_NF;

static {
METAMATA_NF = NumberFormat.getInstance();
METAMATA_NF.setMaximumFractionDigits(1);
@@ -367,30 +380,30 @@ class MetricsElement {

private Vector metrics;

MetricsElement(int indent, String construct, Vector metrics){
MetricsElement(int indent, String construct, Vector metrics) {
this.indent = indent;
this.construct = construct;
this.metrics = metrics;
}

public int getIndent(){
public int getIndent() {
return indent;
}

public String getName(){
public String getName() {
return construct;
}

public Enumeration getMetrics(){
public Enumeration getMetrics() {
return metrics.elements();
}

public boolean isCompilationUnit(){
return ( construct.endsWith(".java") || construct.endsWith(".class") );
public boolean isCompilationUnit() {
return (construct.endsWith(".java") || construct.endsWith(".class"));
}

public boolean isMethod(){
return ( construct.endsWith("(...)") || construct.endsWith("()") );
public boolean isMethod() {
return (construct.endsWith("(...)") || construct.endsWith("()"));
}

public static MetricsElement parse(String line) throws ParseException {
@@ -399,32 +412,32 @@ class MetricsElement {

// i'm using indexOf since I need to know if there are empty strings
// between tabs and I find it easier than with StringTokenizer
while ( (pos = line.indexOf('\t')) != -1 ){
while ((pos = line.indexOf('\t')) != -1) {
String token = line.substring(0, pos);
// only parse what coudl be a valid number. ie not constructs nor no value
/*if (metrics.size() != 0 || token.length() != 0){
Number num = METAMATA_NF.parse(token); // parse with Metamata NF
token = NEUTRAL_NF.format(num.doubleValue()); // and format with a neutral NF
}*/
metrics.addElement( token );
metrics.addElement(token);
line = line.substring(pos + 1);
}
metrics.addElement( line );
metrics.addElement(line);

// there should be exactly 14 tokens (1 name + 13 metrics), if not, there is a problem !
if ( metrics.size() != 14 ){
throw new ParseException("Could not parse the following line as a metrics: -->" + line +"<--", -1);
if (metrics.size() != 14) {
throw new ParseException("Could not parse the following line as a metrics: -->" + line + "<--", -1);
}

// remove the first token it's made of the indentation string and the
// construct name, we'll need all this to figure out what type of
// construct it is since we lost all semantics :(
// (#indent[/]*)(#construct.*)
String name = (String)metrics.elementAt(0);
String name = (String) metrics.elementAt(0);
metrics.removeElementAt(0);
int indent = 0;
pos = name.lastIndexOf('/');
if (pos != -1){
if (pos != -1) {
name = name.substring(pos + 1);
indent = pos + 1; // indentation is last position of token + 1
}


Loading…
Cancel
Save