AjcSpecXmlReader.java
/* *******************************************************************
* Copyright (c) 1999-2001 Xerox Corporation,
* 2002 Palo Alto Research Center, Incorporated (PARC),
* 2003 Contributors.
* All rights reserved.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License v 2.0
* which accompanies this distribution and is available at
* https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
*
* Contributors:
* Xerox/PARC initial implementation
* Wes Isberg resolver
* ******************************************************************/
package org.aspectj.testing.xml;
//import java.util.Vector;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import org.apache.commons.digester3.Digester;
import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.bridge.SourceLocation;
import org.aspectj.testing.harness.bridge.AjcTest;
import org.aspectj.testing.harness.bridge.CompilerRun;
import org.aspectj.testing.harness.bridge.DirChanges;
import org.aspectj.testing.harness.bridge.IncCompilerRun;
import org.aspectj.testing.harness.bridge.JavaRun;
import org.aspectj.testing.harness.bridge.Sandbox;
import org.aspectj.testing.harness.bridge.Validator;
import org.aspectj.testing.util.RunUtils;
import org.aspectj.util.LangUtil;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Read an ajc test specification in xml form.
* Input files should comply with DOCTYPE
*/
public class AjcSpecXmlReader {
/*
* To add new elements or attributes:
* - update the DOCTYPE
* - update setupDigester(..)
* - new sub-elements should be created
* - new attributes should have values set as bean properties
* (possibly by mapping names)
* - new sub-elements should be added to parents
* => the parents need the add method - e.g., add{foo}({foo} toadd)
* - add tests
* - add compile-time checks for mapping APIs in
* setupDigesterComipileTimeCheck
* - when adding an attribute set by bean introspection,
* add to the list returned by expectedProperties()
* - update any client writers referring to the DOCTYPE, as necessary.
* - the parent IXmlWriter should delegate to the child component
* as IXmlWriter (or write the subelement itself)
*
* Debugging
* - use logLevel = 2 for tracing
* - common mistakes
* - dtd has to match input
* - no rule defined (or misdefined) so element ignored
* - property read-only (?)
*/
// private static final String EOL = "\n";
/** presumed relative-path to dtd file for any XML files written by writeSuiteToXmlFile */
public static final String DTD_PATH = "../tests/ajcTestSuite.dtd";
/** expected doc type of AjcSpec XML files */
public static final String DOCTYPE = "<!DOCTYPE "
+ AjcTest.Suite.Spec.XMLNAME + " SYSTEM \"" + DTD_PATH + "\">";
private static final AjcSpecXmlReader ME
= new AjcSpecXmlReader();
/** @return shared instance */
public static final AjcSpecXmlReader getReader() {
return ME;
}
public static void main(String[] a) throws IOException {
writeDTD(new File("../tests/ajcTestSuite2.dtd"));
}
/**
* Write a DTD to dtdFile.
* @deprecated
* @param dtdFile the File to write to
*/
public static void writeDTD(File dtdFile) throws IOException {
LangUtil.throwIaxIfNull(dtdFile, "dtdFile");
PrintWriter out = new PrintWriter(new FileWriter(dtdFile));
try {
out.println("<!-- document type for ajc test suite - see "
+ AjcSpecXmlReader.class.getName() + " -->");
//out.println(getDocType());
} finally {
out.close();
}
}
private static final String[] LOG = new String[] {"info", "debug", "trace" };
// XXX logLevel n>0 causes JUnit tests to fail!
private int logLevel = 0; // use 2 for tracing
private AjcSpecXmlReader() {}
/** @param level 0..2, info..trace */
public void setLogLevel(int level) {
if (level < 0) {
level = 0;
}
if (level > 2) {
level = 2;
}
logLevel = level;
}
/**
* Print an IXmlWritable to the output file
* with our leader and DOCTYPE.
* @param output the File to write to - overwritten
* @return null if no warnings detected, warnings otherwise
*/
public String writeSuiteToXmlFile(File output, IXmlWritable topNode) throws IOException {
PrintWriter writer = new PrintWriter(new FileOutputStream(output));
XMLWriter printSink = new XMLWriter(writer);
writer.println("");
writer.println(AjcSpecXmlReader.DOCTYPE);
writer.println("");
topNode.writeXml(printSink);
writer.close();
String parent = output.getParent();
if (null == parent) {
parent = ".";
}
String dtdPath = parent + "/" + DTD_PATH;
File dtdFile = new File(dtdPath);
if (!dtdFile.canRead()) {
return "expecting dtd file: " + dtdFile.getPath();
}
return null;
}
/**
* Read the specifications for a suite of AjcTest from an XML file.
* This also sets the suite dir in the specification.
* @param file the File must be readable, comply with DOCTYPE.
* @return AjcTest.Suite.Spec read from file
* @see #setLogLevel(int)
*/
public AjcTest.Suite.Spec readAjcSuite(File file) throws IOException, AbortException {
// setup loggers for digester and beanutils...
System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); // XXX
System.setProperty("org.apache.commons.logging.simplelog.defaultlog", LOG[logLevel]); // trace debug XXX
final Digester digester = makeDigester(file);
SuiteHolder holder = new SuiteHolder();
digester.push(holder);
FileInputStream input = new FileInputStream(file);
try {
digester.parse(input);
} catch (SAXException e) {
MessageUtil.fail("parsing " + file, e);
} finally {
if (null != input) {
input.close();
input = null;
}
}
AjcTest.Suite.Spec result = holder.spec;
if (null != result) {
file = file.getAbsoluteFile();
result.setSourceLocation(new SourceLocation(file, 1));
File suiteDir = file.getParentFile();
if (null == suiteDir) {
// should not be the case if absolute
suiteDir = new File("."); // user.dir?
}
result.setSuiteDirFile(suiteDir);
if (result.runtime.isVerbose()) { // XXX hack fixup
RunUtils.enableVerbose(result);
}
}
return result;
}
private Digester makeDigester(final File suiteFile) {
// implement EntityResolver directly; set is failing
Digester result = new Digester() {
final SuiteResolver resolver = new SuiteResolver(suiteFile);
public InputSource resolveEntity(
String publicId,
String systemId)
throws SAXException {
return resolver.resolveEntity(publicId, systemId);
}
};
setupDigester(result);
return result;
}
/** set up the mapping between the xml and Java. */
private void setupDigester(Digester digester) {
// XXX supply sax parser to ignore white space?
digester.setValidating(true);
// try {
// // this is the correct approach, but the commons parser
// // fails to accept a second, overriding registration - see
// // https://lists.xml.org/archives/xml-dev/200111/msg00959.html
// digester.getXMLReader().setEntityResolver(new SuiteResolver(suiteFile));
// } catch (SAXException e) {
// System.err.println("unable to set entity resolver");
// e.printStackTrace(System.err);
// }
// element names come from the element components
final String suiteX = AjcTest.Suite.Spec.XMLNAME;
final String ajctestX = suiteX + "/" + AjcTest.Spec.XMLNAME;
final String compileX = ajctestX + "/" + CompilerRun.Spec.XMLNAME;
final String inccompileX = ajctestX + "/" + IncCompilerRun.Spec.XMLNAME;
final String runX = ajctestX + "/" + JavaRun.Spec.XMLNAME;
final String dirchangesX = "*/" + DirChanges.Spec.XMLNAME;
final String messageX = "*/" + SoftMessage.XMLNAME;
final String messageSrcLocX = messageX + "/" +SoftSourceLocation.XMLNAME;
// ---- each sub-element needs to be created
// handle messages the same at any level
digester.addObjectCreate(suiteX, AjcTest.Suite.Spec.class.getName());
digester.addObjectCreate(ajctestX, AjcTest.Spec.class.getName());
digester.addObjectCreate(compileX, CompilerRun.Spec.class.getName());
//digester.addObjectCreate(compileX + "/file", AbstractRunSpec.WrapFile.class.getName());
digester.addObjectCreate(inccompileX, IncCompilerRun.Spec.class.getName());
digester.addObjectCreate(runX, JavaRun.Spec.class.getName());
digester.addObjectCreate(messageX, SoftMessage.class.getName());
digester.addObjectCreate(messageSrcLocX, SoftSourceLocation.class.getName());
digester.addObjectCreate(dirchangesX, DirChanges.Spec.class.getName());
// ---- set bean properties for sub-elements created automatically
// -- some remapped - warnings
// - if property exists, map will not be used
digester.addSetProperties(suiteX); // ok to have suite messages and global suite options, etc.
digester.addSetProperties(ajctestX,
new String[] { "title", "dir", "pr"},
new String[] { "description", "testDirOffset", "bugId"});
digester.addSetProperties(compileX,
new String[] { "files", "argfiles"},
new String[] { "paths", "argfiles"});
digester.addSetProperties(compileX + "/file");
digester.addSetProperties(inccompileX, "classes", "paths");
digester.addSetProperties(runX,
new String[] { "class", "vm", "skipTester", "fork", "vmargs", "aspectpath", "module"},
new String[] { "className", "javaVersion", "skipTester", "fork", "vmArgs", "aspectpath", "module"});
digester.addSetProperties(dirchangesX);
digester.addSetProperties(messageX);
digester.addSetProperties(messageSrcLocX, "line", "lineAsString");
digester.addSetProperties(messageX, "kind", "kindAsString");
digester.addSetProperties(messageX, "line", "lineAsString");
//digester.addSetProperties(messageX, "details", "details");
// only file subelement of compile uses text as path... XXX vestigial
digester.addCallMethod(compileX + "/file", "setFile", 0);
// ---- when subelements are created, add to parent
// add ajctest to suite, runs to ajctest, files to compile, messages to any parent...
// the method name (e.g., "addSuite") is in the parent (SuiteHolder)
// the class (e.g., AjcTest.Suite.Spec) refers to the type of the object created
digester.addSetNext(suiteX, "addSuite", AjcTest.Suite.Spec.class.getName());
digester.addSetNext(ajctestX, "addChild", AjcTest.Spec.class.getName());
digester.addSetNext(compileX, "addChild", CompilerRun.Spec.class.getName());
digester.addSetNext(inccompileX, "addChild", IncCompilerRun.Spec.class.getName());
digester.addSetNext(runX, "addChild", JavaRun.Spec.class.getName());
//digester.addSetNext(compileX + "/file", "addWrapFile", AbstractRunSpec.WrapFile.class.getName());
digester.addSetNext(messageX, "addMessage", IMessage.class.getName());
// setSourceLocation is for the inline variant
// addSourceLocation is for the extra
digester.addSetNext(messageSrcLocX, "addSourceLocation", ISourceLocation.class.getName());
digester.addSetNext(dirchangesX, "addDirChanges", DirChanges.Spec.class.getName());
// can set parent, but prefer to have "knows-about" flow down only...
}
// ------------------------------------------------------------ testing code
/**
* Get expected bean properties for introspection tests.
* This should return an expected property for every attribute in DOCTYPE,
* using any mapped-to names from setupDigester.
*/
static BProps[] expectedProperties() {
return new BProps[]
{
new BProps(AjcTest.Suite.Spec.class,
new String[] { "suiteDir"}), // verbose removed
new BProps(AjcTest.Spec.class,
new String[] { "description", "testDirOffset", "bugId"}),
// mapped from { "title", "dir", "pr"}
new BProps(CompilerRun.Spec.class,
new String[] { "files", "options",
"staging", "badInput", "reuseCompiler", "includeClassesDir",
"argfiles", "aspectpath", "classpath", "extdirs",
"sourceroots", "xlintfile", "outjar"}),
new BProps(IncCompilerRun.Spec.class,
new String[] { "tag" }),
new BProps(JavaRun.Spec.class,
new String[] { "className", "skipTester", "options",
"javaVersion", "errStreamIsError", "outStreamIsError",
"fork", "vmArgs", "aspectpath"}),
// mapped from { "class", ...}
new BProps(DirChanges.Spec.class,
new String[] { "added", "removed", "updated", "unchanged", "dirToken", "defaultSuffix"}),
// new BProps(AbstractRunSpec.WrapFile.class,
// new String[] { "path"}),
new BProps(SoftMessage.class,
new String[] { "kindAsString", "lineAsString", "text", "details", "file"})
// mapped from { "kind", "line", ...}
};
}
/**
* This is only to do compile-time checking for the APIs impliedly
* used in setupDigester(..).
* The property setter checks are redundant with tests based on
* expectedProperties().
*/
private static void setupDigesterCompileTimeCheck() {
if (true) { throw new Error("never invoked"); }
AjcTest.Suite.Spec suite = new AjcTest.Suite.Spec();
AjcTest.Spec test = new AjcTest.Spec();
Sandbox sandbox = null;
Validator validator = null;
// AjcTest ajctest = new AjcTest(test, sandbox, validator);
// ajctest.addRunSpec((AbstractRunSpec) null);
//// test.makeIncCompilerRun((IncCompilerRun.Spec) null);
//// test.makeJavaRun((JavaRun.Spec) null);
// ajctest.setDescription((String) null);
// ajctest.setTestBaseDirOffset((String) null);
// ajctest.setBugId((String) null);
// ajctest.setTestSourceLocation((ISourceLocation) null);
CompilerRun.Spec crunSpec = new CompilerRun.Spec();
crunSpec.addMessage((IMessage) null);
// XXX crunSpec.addSourceLocation((ISourceLocation) null);
// crunSpec.addWrapFile((AbstractRunSpec.WrapFile) null);
crunSpec.setOptions((String) null);
crunSpec.setPaths((String) null);
crunSpec.setIncludeClassesDir(false);
crunSpec.setReuseCompiler(false);
crunSpec.setXlintfile((String) null);
crunSpec.setOutjar((String)null);
IncCompilerRun.Spec icrunSpec = new IncCompilerRun.Spec();
icrunSpec.addMessage((IMessage) null);
icrunSpec.setTag((String) null);
icrunSpec.setFresh(false);
JavaRun.Spec jrunspec = new JavaRun.Spec();
jrunspec.addMessage((IMessage) null);
jrunspec.setClassName((String) null);
jrunspec.addMessage((IMessage) null);
// input s.b. interpretable by Boolean.valueOf(String)
jrunspec.setSkipTester(true);
jrunspec.setErrStreamIsError("false");
jrunspec.setOutStreamIsError("false");
jrunspec.setAspectpath("");
jrunspec.setClasspath("");
jrunspec.setFork(false);
jrunspec.setLTW("false");
jrunspec.setException("Error");
DirChanges.Spec dcspec = new DirChanges.Spec();
dcspec.setAdded((String) null);
dcspec.setRemoved((String) null);
dcspec.setUpdated((String) null);
dcspec.setDefaultSuffix((String) null);
dcspec.setDirToken((String) null);
SoftMessage m = new SoftMessage();
m.setSourceLocation((ISourceLocation) null);
m.setText((String) null);
m.setKindAsString((String) null);
m.setDetails((String) null);
SoftSourceLocation sl = new SoftSourceLocation();
sl.setFile((String) null);
sl.setLine((String) null);
sl.setColumn((String) null);
sl.setEndLine((String) null);
// add attribute setters to validate?
}
/** top element on Digester stack holds the test suite */
public static class SuiteHolder {
AjcTest.Suite.Spec spec;
public void addSuite(AjcTest.Suite.Spec spec) {
this.spec = spec;
}
}
/** hold class/properties association for testing */
static class BProps {
final Class cl;
final String[] props;
BProps(Class cl, String[] props) {
this.cl = cl;
this.props = props;
}
}
/**
* Find file NAME=="ajcTestSuite.dtd" from some reasonably-local
* relative directories.
* XXX bug: commons parser doesn't accept second registration,
* so we override Digester's implementation instead.
* XXX cannot JUnit test SuiteResolver since they run from
* local directory with valid reference
* XXX does not fix JDK 1.4 parser message "unable to resolve without base URI"
* XXX should be able to just set BaseURI instead...
*/
static class SuiteResolver implements EntityResolver {
public static final String NAME = "ajcTestSuite.dtd";
private static boolean isDir(File dir) {
return ((null != dir) && dir.canRead() && dir.isDirectory());
}
private final File suiteFile;
public SuiteResolver(File suiteFile) {
this.suiteFile = suiteFile;
}
private String getPath(String id) {
// first, try id relative to suite file
final File suiteFileDir = suiteFile.getParentFile();
if (isDir(suiteFileDir)) {
String path = suiteFileDir.getPath()
+ "/" + id;
File result = new File(path);
if (result.canRead() && result.isFile()) {
return result.getPath();
}
}
// then try misc paths relative to suite file or current dir
final File[] baseDirs = new File[]
{ suiteFileDir, new File(".")
};
final String[] locations = new String[]
{ ".", "..", "../tests", "../../tests",
"../../../tests", "tests", "modules/tests"
};
File baseDir;
for (File file : baseDirs) {
baseDir = file;
if (!isDir(baseDir)) {
continue;
}
for (String location : locations) {
File dir = new File(baseDir, location);
if (isDir(dir)) {
File temp = new File(dir, NAME);
if (temp.isFile() && temp.canRead()) {
return temp.getPath();
}
}
}
}
return null;
}
public InputSource resolveEntity(
String publicId,
String systemId)
throws SAXException {
InputSource result = null;
if ((null != systemId) &&
systemId.endsWith(NAME)) {
String path = getPath(systemId);
if (null != path) {
result = new InputSource(path);
result.setSystemId(path);
result.setPublicId(path);
}
}
return result;
}
}
}
//private String getDocTypePath() {
// String result = null;
// if (null != suiteFile) {
// FileReader fr = null;
// try {
// fr = new FileReader(suiteFile);
// BufferedReader reader = new BufferedReader(fr);
// String line;
// while (null != (line = reader.readLine())) {
// String upper = line.trim();
// line = upper.toLowerCase();
// if (line.startsWith("<")) {
// if (line.startsWith("<!doctype ")) {
// int start = line.indexOf("\"");
// int end = line.lastIndexOf(NAME + "\"");
// if ((0 < start) && (start < end)) {
// return upper.substring(start+1,
// end + NAME.length());
// }
// } else if (!line.startsWith("<?xml")) {
// break; // something else...
// }
// }
// }
// } catch (IOException e) {
// // ignore
// } finally {
// if (null != fr) {
// try {
// fr.close();
// } catch (IOException e1) { // ignore
// }
// }
// }
// }
// return null;
//}