/*
* Copyright (c) 2004-2005 Ant-Contrib project. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sf.antcontrib.design;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Vector;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.DescendingVisitor;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Utility;
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.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;
import org.apache.tools.ant.util.JAXPUtils;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
/**
*
*
*
* @author dhiller
*
*/
public class VerifyDesignDelegate implements Log {
private File designFile;
private Vector paths = new Vector();
private boolean isCircularDesign = false;
private boolean deleteFiles = false;
private boolean fillInBuildException = false;
private boolean needDeclarationsDefault = true;
private boolean needDependsDefault = true;
private Task task;
private Design design;
private HashSet primitives = new HashSet();
private Vector designErrors = new Vector();
private boolean verifiedAtLeastOne = false;
public VerifyDesignDelegate(Task task) {
this.task = task;
primitives.add("B");
primitives.add("C");
primitives.add("D");
primitives.add("F");
primitives.add("I");
primitives.add("J");
primitives.add("S");
primitives.add("Z");
}
public void addConfiguredPath(Path path) {
// Path newPath = new Path(task.getProject());
// path.
paths.add(path);
}
public void setJar(File f) {
Path p = (Path)task.getProject().createDataType("path");
p.createPathElement().setLocation(f.getAbsoluteFile());
addConfiguredPath(p);
}
public void setDesign(File f) {
this.designFile = f;
}
public void setCircularDesign(boolean isCircularDesign) {
this.isCircularDesign = isCircularDesign;
}
public void setDeleteFiles(boolean deleteFiles) {
this.deleteFiles = deleteFiles;
}
public void setFillInBuildException(boolean b) {
fillInBuildException = b;
}
public void setNeedDeclarationsDefault(boolean b) {
needDeclarationsDefault = b;
}
public void setNeedDependsDefault(boolean b) {
needDependsDefault = b;
}
public void execute() {
if(!designFile.exists() || designFile.isDirectory())
throw new BuildException("design attribute in verifydesign element specified an invalid file="+designFile);
verifyJarFilesExist();
try {
XMLReader reader = JAXPUtils.getXMLReader();
DesignFileHandler ch = new DesignFileHandler(this, designFile, isCircularDesign, task.getLocation());
ch.setNeedDeclarationsDefault(needDeclarationsDefault);
ch.setNeedDependsDefault(needDependsDefault);
reader.setContentHandler(ch);
//reader.setEntityResolver(ch);
//reader.setErrorHandler(ch);
//reader.setDTDHandler(ch);
log("about to start parsing file='"+designFile+"'", Project.MSG_INFO);
FileInputStream fileInput = new FileInputStream(designFile);
InputSource src = new InputSource(fileInput);
reader.parse(src);
design = ch.getDesign();
Enumeration pathsEnum = paths.elements();
Path p = null;
while (pathsEnum.hasMoreElements()) {
p = (Path)pathsEnum.nextElement();
verifyPathAdheresToDesign(design, p);
}
//only put unused errors if there are no other errors
//this is because you end up with false unused errors if you don't do this.
if(designErrors.isEmpty())
design.fillInUnusedPackages(designErrors);
if (! designErrors.isEmpty()) {
log(designErrors.size()+"Errors.", Project.MSG_WARN);
if(!fillInBuildException)
throw new BuildException("Design check failed due to previous errors");
throwAllErrors();
}
} catch (SAXException e) {
maybeDeleteFiles();
if (e.getException() != null
&& e.getException() instanceof RuntimeException)
throw (RuntimeException) e.getException();
else if (e instanceof SAXParseException) {
SAXParseException pe = (SAXParseException) e;
throw new BuildException("\nProblem parsing design file='"
+ designFile + "'. \nline=" + pe.getLineNumber()
+ " column=" + pe.getColumnNumber() + " Reason:\n"
+ e.getMessage() + "\n", e);
}
throw new BuildException("\nProblem parsing design file='"
+ designFile + "'. Reason:\n" + e, e);
} catch (IOException e) {
maybeDeleteFiles();
throw new RuntimeException("See attached exception", e);
// throw new BuildException("IOException on design file='"
// + designFile + "'. attached:", e);
} catch(RuntimeException e) {
maybeDeleteFiles();
throw e;
} finally {
}
if(!verifiedAtLeastOne)
throw new BuildException("Did not find any class or jar files to verify");
}
//some auto builds like cruisecontrol can only report all the
//standard ant task errors and the build exceptions so here
//we need to fill in the buildexception so the errors are reported
//correctly through those tools....though, you think ant has a hook
//in that cruisecontrol is not using like LogListeners or something
private void throwAllErrors() {
String result = "Design check failed due to following errors";
Enumeration exceptions = designErrors.elements();
while(exceptions.hasMoreElements()) {
BuildException be = (BuildException)exceptions.nextElement();
String message = be.getMessage();
result += "\n" + message;
}
throw new BuildException(result);
}
private void verifyJarFilesExist() {
Enumeration pathsEnum = paths.elements();
Path p = null;
while (pathsEnum.hasMoreElements()) {
p = (Path)pathsEnum.nextElement();
String files[] = p.list();
for (int i=0;i<files.length;i++) {
File file = new File(files[i]);
if (!file.exists())
throw new BuildException(VisitorImpl.getNoFileMsg(file));
}
}
}
private void maybeDeleteFiles() {
if (deleteFiles) {
log("Deleting all class and jar files so you do not get tempted to\n" +
"use a jar that doesn't abide by the design(This option can\n" +
"be turned off if you really want)", Project.MSG_INFO);
Enumeration pathsEnum = paths.elements();
Path p = null;
while (pathsEnum.hasMoreElements()) {
p = (Path)pathsEnum.nextElement();
deleteFilesInPath(p);
}
}
}
private void deleteFilesInPath(Path p) {
String files[] = p.list();
for (int i=0;i<files.length;i++) {
File file = new File(files[i]);
boolean deleted = file.delete();
if (! deleted) {
file.deleteOnExit();
}
}
}
private void verifyPathAdheresToDesign(Design d, Path p) throws ClassFormatException, IOException {
String files[] = p.list();
for (int i=0;i<files.length;i++) {
File file = new File(files[i]);
if(file.isDirectory()) {
FileSet set = new FileSet();
set.setDir(file);
set.setProject(task.getProject());
PatternSet.NameEntry entry1 = set.createInclude();
PatternSet.NameEntry entry2 = set.createInclude();
PatternSet.NameEntry entry3 = set.createInclude();
entry1.setName("**/*.class");
entry2.setName("**/*.jar");
entry3.setName("**/*.war");
DirectoryScanner scanner = set.getDirectoryScanner(task.getProject());
scanner.setBasedir(file);
String[] scannerFiles = scanner.getIncludedFiles();
for(int j = 0; j < scannerFiles.length; j++) {
verifyPartOfPath(scannerFiles[j], new File(file, scannerFiles[j]), d);
}
} else
verifyPartOfPath(files[i], file, d);
}
}
private void verifyPartOfPath(String fileName, File file, Design d) throws IOException {
if (fileName.endsWith(".jar") || fileName.endsWith(".war")) {
JarFile jarFile = new JarFile(file);
verifyJarAdheresToDesign(d, jarFile, file);
} else if (fileName.endsWith(".class")) {
verifyClassAdheresToDesign(d, file);
} else
throw new BuildException("Only directories, jars, wars, and class files can be supplied to verify design, not file="+file.getAbsolutePath());
}
private void verifyClassAdheresToDesign(Design d, File classFile)
throws ClassFormatException, IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream(classFile);
verifyClassAdheresToDesign(d, fis, classFile.getAbsolutePath(), classFile);
}
finally {
try {
if (fis != null) {
fis.close();
}
}
catch (IOException e) {
; //doh!!
}
}
}
private void verifyJarAdheresToDesign(Design d, JarFile jarFile, File original)
throws ClassFormatException, IOException {
try {
Enumeration en = jarFile.entries();
while(en.hasMoreElements()) {
ZipEntry entry = (ZipEntry)en.nextElement();
InputStream in = null;
if(entry.getName().endsWith(".class")) {
in = jarFile.getInputStream(entry);
try {
in = jarFile.getInputStream(entry);
verifyClassAdheresToDesign(d, in, entry.getName(), original);
}
finally {
try {
if (in != null) {
in.close();
}
}
catch (IOException e) {
; // doh!!!
}
}
}
}
}
finally {
try {
jarFile.close();
}
catch (IOException e) {
; //doh!!!
}
}
}
private String className = "";
private void verifyClassAdheresToDesign(Design d, InputStream in, String name, File originalClassOrJarFile) throws ClassFormatException, IOException {
try {
verifiedAtLeastOne = true;
ClassParser parser = new ClassParser(in, name);
JavaClass javaClass = parser.parse();
className = javaClass.getClassName();
if(!d.needEvalCurrentClass(className))
return;
ConstantPool pool = javaClass.getConstantPool();
processConstantPool(pool);
VisitorImpl visitor = new VisitorImpl(pool, this, d, task.getLocation());
DescendingVisitor desc = new DescendingVisitor(javaClass, visitor);
desc.visit();
} catch(BuildException e) {
log(Design.getWrapperMsg(originalClassOrJarFile, e.getMessage()), Project.MSG_ERR);
designErrors.addElement(e);
}
}
private void processConstantPool(ConstantPool pool) {
Constant[] constants = pool.getConstantPool();
if(constants == null) {
log(" constants=null", Project.MSG_VERBOSE);
return;
}
log(" constants len="+constants.length, Project.MSG_VERBOSE);
for(int i = 0; i < constants.length; i++) {
processConstant(pool, constants[i], i);
}
}
private void processConstant(ConstantPool pool, Constant c, int i) {
if(c == null) //don't know why, but constant[0] seems to be always null.
return;
log(" const["+i+"]="+pool.constantToString(c)+" inst="+c.getClass().getName(), Project.MSG_DEBUG);
byte tag = c.getTag();
switch(tag) {
//reverse engineered from ConstantPool.constantToString..
case Constants.CONSTANT_Class:
int ind = ((ConstantClass)c).getNameIndex();
c = pool.getConstant(ind, Constants.CONSTANT_Utf8);
String className = Utility.compactClassName(((ConstantUtf8)c).getBytes(), false);
log(" classNamePre="+className, Project.MSG_DEBUG);
className = getRidOfArray(className);
String firstLetter = className.charAt(0)+"";
if(primitives.contains(firstLetter))
return;
log(" className="+className, Project.MSG_VERBOSE);
design.checkClass(className);
break;
default:
}
}
private static String getRidOfArray(String className) {
while(className.startsWith("["))
className = className.substring(1, className.length());
return className;
}
public static String getPackageName(String className) {
String packageName = Package.DEFAULT;
int index = className.lastIndexOf(".");
if(index > 0)
packageName = className.substring(0, index);
//DEANDO: test the else scenario here(it is a corner case)...
return packageName;
}
public void log(String msg, int level) {
//if(level == Project.MSG_WARN || level == Project.MSG_INFO
// || level == Project.MSG_ERR || level == Project.MSG_VERBOSE)
//VerifyDesignTest.log(msg);
task.log(msg, level);
}
}