/*
* Copyright 2006 Ethan Nicholas. All rights reserved.
* Use is subject to license terms.
*/
package jaxx.compiler;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.List;
import java.util.regex.*;
import java.util.zip.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.sax.*;
import org.w3c.dom.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import jaxx.*;
import jaxx.css.*;
import jaxx.parser.ParseException;
import jaxx.reflect.*;
import jaxx.runtime.*;
import jaxx.runtime.swing.*;
import jaxx.spi.*;
import jaxx.tags.*;
import jaxx.types.*;
/** Compiles JAXX files into Java classes. */
public class JAXXCompiler {
/** True to throw exceptions when we encounter unresolvable classes, false to ignore.
* This is currently set to false until JAXX has full support for inner classes
* (including enumerations), because currently they don't always resolve (but will
* generally compile without error anyway).
*/
public static final boolean STRICT_CHECKS = false;
public static final String JAXX_NAMESPACE = "http://www.jaxxframework.org/";
public static final String JAXX_INTERNAL_NAMESPACE = "http://www.jaxxframework.org/internal";
/** Maximum length of an inlinable creation method. */
private static final int INLINE_THRESHOLD = 300;
/** Contains import declarations (of the form "javax.swing.") which are always imported in all compiler instances. */
private static List/*<String>*/ staticImports = new ArrayList/*<String>*/();
static {
staticImports.add("java.awt.*");
staticImports.add("java.awt.event.*");
staticImports.add("java.beans.*");
staticImports.add("java.io.*");
staticImports.add("java.lang.*");
staticImports.add("java.util.*");
staticImports.add("javax.swing.*");
staticImports.add("javax.swing.border.*");
staticImports.add("javax.swing.event.*");
staticImports.add("jaxx.runtime.swing.JAXXButtonGroup");
staticImports.add("jaxx.runtime.swing.HBox");
staticImports.add("jaxx.runtime.swing.VBox");
staticImports.add("jaxx.runtime.swing.Table");
}
private static DefaultObjectHandler firstPassClassTagHandler = new DefaultObjectHandler(ClassDescriptorLoader.getClassDescriptor(Object.class));
/** A list of Runnables which will be run after the first compilation pass. This is primarily used
* to trigger the creation of CompiledObjects, which cannot be created during the first pass and must be
* created in document order.
*/
private List/*<Runnable>*/ initializers = new ArrayList/*<Runnable>*/();
/** Files being compiled during the compilation session, may be modified as compilation progresses and additional dependencies are found. */
private static List/*<File>*/ jaxxFiles = new ArrayList/*<File>*/();
/** Class names corresponding to the files in the jaxxFiles list. */
private static List/*<String>*/ jaxxFileClassNames = new ArrayList/*<String>*/();
/** Maps the names of classes being compiled to the compiler instance handling the compilation. */
private static Map/*<String, JAXXCompiler>*/ compilers = new HashMap/*<String, JAXXCompiler>*/();
/** Maps the names of classes being compiled to their symbol tables (created after the first compiler pass). */
private static Map/*<String, SymbolTable>*/ symbolTables = new HashMap/*<String, SymbolTable>*/();
private CompilerOptions options;
/** Used for error reporting purposes, so we can report the right line number. */
private Stack/*<Element>*/ tagsBeingCompiled = new Stack/*<Element>*/();
/** Used for error reporting purposes, so we can report the right source file. */
private Stack/*<File>*/ sourceFiles = new Stack/*<File>*/();
/** Maps object ID strings to the objects themselves. These are created during the second compilation pass. */
private Map/*<String, CompiledObject>*/ objects = new LinkedHashMap/*<String, CompiledObject>*/();
/** Maps objects to their ID strings. These are created during the second compilation pass. */
private Map/*<CompiledObject, String>*/ ids = new LinkedHashMap/*<CompiledObject, String>*/();
private static int errorCount;
private static int warningCount;
private boolean failed;
/** Object corresponding to the root tag in the document. */
private CompiledObject root;
/** Contains strings of the form "javax.swing." */
private Set/*<String>*/ importedPackages = new HashSet/*<String>*/();
/** Contains strings of the form "javax.swing.Timer" */
private Set/*<String>*/ importedClasses = new HashSet/*<String>*/();
/** Keeps track of open components (components still having children added). */
private Stack/*<CompiledObject>*/ openComponents = new Stack/*<CompiledObject>*/();
/** Sequence number used to create automatic variable names. */
private int autogenID = 0;
private List/*<DataBinding>*/ dataBindings = new ArrayList/*<DataBinding>*/();
private JavaFile javaFile = new JavaFile();
// true if a main() method has been declared in a script
boolean mainDeclared;
private SymbolTable symbolTable = new SymbolTable();
// TODO: replace these public StringBuffers with something a little less stupid
/** Extra code to be added to the instance initializer. */
public StringBuffer initializer = new StringBuffer();
/** Extra code to be added at the end of the instance initializer. */
public StringBuffer lateInitializer = new StringBuffer();
/** Extra code to be added to the class body. */
public StringBuffer bodyCode = new StringBuffer();
/** Code to initialize data bindings. */
public StringBuffer initDataBindings = new StringBuffer();
/** Body of the applyDataBinding method. */
public StringBuffer applyDataBinding = new StringBuffer();
/** Body of the removeDataBinding method. */
public StringBuffer removeDataBinding = new StringBuffer();
/** Body of the processDataBinding method. */
public StringBuffer processDataBinding = new StringBuffer();
/** Base directory used for path resolution (normally the directory in which the .jaxx file resides). */
private File baseDir;
/** .jaxx file being compiled. */
private File src;
/** Generated .java file. */
private File dest;
/** Parsed XML of src file. */
private Document document;
/** Name of class being compiled. */
private String outputClassName;
private ScriptManager scriptManager = new ScriptManager(this);
/** Combination of all stylesheets registered using {@link #registerStylesheet}. */
private Stylesheet stylesheet;
/** Contains all attributes defined inline on class tags. */
private List/*<Rule>*/ inlineStyles = new ArrayList/*<Rule>*/();
/** Maps objects (expressed in Java code) to event listener classes (e.g. MouseListener) to Lists of EventHandlers. The final list
* contains all event handlers of a particular type attached to a particular object (again, as represented by a Java expression). */
private Map/*<String, Map<ClassDescriptor, List<EventHandler>>>*/ eventHandlers = new HashMap/*<CString, Map<ClassDescriptor, List<EventHandler>>>*/();
private Map/*<Object, String>*/ uniqueIds = new HashMap/*<Object, String>*/();
private Map/*<EventHandler, String>*/ eventHandlerMethodNames = new HashMap/*<EventHandler, String>*/();
/** ClassLoader which searches the user-specified class path in addition to the normal class path */
private ClassLoader classLoader;
private static final int PASS_1 = 0;
private static final int PASS_2 = 1;
private static int currentPass;
static {
try {
loadLibraries();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void init() {
// forces static initializer to run if it hasn't yet
}
public static void loadLibraries() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
Enumeration/*<URL>*/ e = JAXXCompiler.class.getClassLoader().getResources("jaxx.properties");
while (e.hasMoreElements()) {
Properties p = new Properties();
InputStream in = ((URL) e.nextElement()).openConnection().getInputStream();
p.load(in);
in.close();
String initializer = p.getProperty("jaxx.initializer");
if (initializer != null)
((Initializer) Class.forName(initializer).newInstance()).initialize();
}
}
private JAXXCompiler(ClassLoader classLoader) {
this.options = new CompilerOptions();
this.classLoader = classLoader;
addImport("java.lang.*");
}
/** Creates a new JAXXCompiler.
*/
protected JAXXCompiler(File baseDir, File src, String outputClassName, CompilerOptions options) {
this.baseDir = baseDir;
this.src = src;
sourceFiles.push(src);
this.outputClassName = outputClassName;
this.options = options;
addImport(outputClassName.substring(0, outputClassName.lastIndexOf(".") + 1) + "*");
Iterator/*<String>*/ i = staticImports.iterator();
while (i.hasNext())
addImport((String) i.next());
}
/** Creates a dummy JAXXCompiler for use in unit testing. */
public static JAXXCompiler createDummyCompiler() {
return createDummyCompiler(JAXXCompiler.class.getClassLoader());
}
/** Creates a dummy JAXXCompiler for use in unit testing. */
public static JAXXCompiler createDummyCompiler(ClassLoader classLoader) {
return new JAXXCompiler(classLoader);
}
public CompilerOptions getOptions() {
return options;
}
public JavaFile getJavaFile() {
return javaFile;
}
private void compileFirstPass() throws IOException {
try {
InputStream in = new FileInputStream(src);
document = parseDocument(in);
in.close();
compileFirstPass(document.getDocumentElement());
}
catch (SAXParseException e) {
reportError(e.getLineNumber(), "Invalid XML: " + e.getMessage());
}
catch (SAXException e) {
reportError(null, "Error parsing XML document: " + e);
}
}
private void runInitializers() {
Iterator/*<Runnable>*/ i = initializers.iterator();
while (i.hasNext()) {
((Runnable) i.next()).run();
i.remove();
}
}
/** Registers a <code>Runnable</code> which will be executed after the first
* compilation pass is complete.
*/
public void registerInitializer(Runnable r) {
initializers.add(r);
}
private void compileSecondPass() throws IOException {
if (!tagsBeingCompiled.isEmpty())
throw new RuntimeException("Internal error: starting pass two, but tagsBeingCompiled is not empty: " + tagsBeingCompiled);
compileSecondPass(document.getDocumentElement());
}
private void applyStylesheets() {
Iterator/*<CompiledObject>*/ i = new ArrayList/*<CompiledObject>*/(objects.values()).iterator();
while (i.hasNext()) {
CompiledObject object = (CompiledObject) i.next();
TagManager.getTagHandler(object.getObjectClass()).applyStylesheets(object, this);
}
}
private void generateCode() throws IOException {
if (options.getTargetDirectory() != null)
dest = new File(options.getTargetDirectory(), outputClassName.replace('.', File.separatorChar) + ".java");
else
dest = new File(baseDir, outputClassName.substring(outputClassName.lastIndexOf(".") + 1) + ".java");
PrintWriter out = new PrintWriter(new FileWriter(dest));
createJavaSource(out);
out.close();
}
private void runJavac() {
try {
URL jaxxURL = getClass().getResource("/jaxx/compiler/JAXXCompiler.class");
if (jaxxURL == null)
throw new InternalError("Can't-happen error: could not find /jaxx/compiler/JAXXCompiler.class on class path");
String classpath = jaxxURL.toString();
if (classpath.startsWith("jar:")) {
classpath = classpath.substring("jar:".length());
classpath = classpath.substring(0, classpath.indexOf("!"));
classpath = URLtoFile(classpath).getPath();
}
URL runtimeURL = getClass().getResource("/jaxx/runtime/JAXXObject.class");
if (runtimeURL == null)
throw new InternalError("Can't-happen error: could not find /jaxx/runtime/JAXXObject.class on class path");
String runtime = runtimeURL.toString();
if (runtime.startsWith("jar:")) {
runtime = runtime.substring("jar:".length());
runtime = runtime.substring(0, runtime.indexOf("!"));
runtime = URLtoFile(runtime).getPath();
}
classpath += File.pathSeparator + runtime + File.pathSeparator + options.getClassPath() + File.pathSeparator + ".";
Class javac = Class.forName("com.sun.tools.javac.Main");
Method main = javac.getMethod("compile", new Class[] { String[].class });
final PrintStream oldErr = System.err;
System.setErr(new PrintStream(new FilterOutputStream(oldErr) {
public void write(byte[] b, int off, int len) throws IOException {
String stringValue = new String(b, off, len).trim();
if (stringValue.startsWith("Error:") || stringValue.startsWith("Usage:") || stringValue.endsWith("error") ||
stringValue.endsWith("errors"))
failed = true;
if (stringValue.endsWith("uses unchecked or unsafe operations.") ||
stringValue.endsWith("with -Xlint:unchecked for details."))
return;
super.write(b, off, len);
}
}));
List/*<String>*/ javacOpts = new ArrayList();
if (options.getJavacOpts() != null)
javacOpts.addAll(Arrays.asList(options.getJavacOpts().split("\\s+")));
if (options.getTargetDirectory() != null) {
String destRoot = options.getTargetDirectory().getPath();
javacOpts.add("-d");
javacOpts.add(destRoot);
classpath += File.pathSeparator + destRoot;
}
javacOpts.add("-classpath");
javacOpts.add(classpath);
javacOpts.add(dest.getPath());
main.invoke(null, new Object[] { javacOpts.toArray(new String[javacOpts.size()]) });
System.setErr(oldErr);
}
catch (ClassNotFoundException e) {
System.err.println("Unable to find javac (com.sun.tools.javac.Main) on class path.");
System.err.println("jaxxc launch script is responsible for adding javac (typically");
System.err.println("located in tools.jar) to the class path; it either added the");
System.err.println("wrong path or tools.jar does not exist.");
System.err.println();
System.err.println("Check to make sure that JAVA_HOME points to a valid JDK");
System.err.println("installation.");
failed = true;
}
catch (Exception e) {
System.err.println("An error occurred while invoking javac:");
e.printStackTrace();
failed = true;
}
if (!options.getKeepJavaFiles())
dest.delete();
}
private void createJavaSource(PrintWriter out) throws IOException {
int dotPos = outputClassName.lastIndexOf(".");
String packageName = dotPos != -1 ? outputClassName.substring(0, dotPos) : null;
String simpleClassName = outputClassName.substring(dotPos + 1);
outputClass(packageName, simpleClassName, out);
}
public String getOutputClassName() {
return outputClassName;
}
public static SAXParser getSAXParser() {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
SAXParser parser = factory.newSAXParser();
return parser;
}
catch (SAXException e) {
throw new RuntimeException(e);
}
catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
}
public static Document parseDocument(InputStream in) throws IOException, SAXException {
try {
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer();
transformer.setErrorListener(new ErrorListener() {
public void warning(TransformerException ex) throws TransformerException {
throw ex;
}
public void error(TransformerException ex) throws TransformerException {
throw ex;
}
public void fatalError(TransformerException ex) throws TransformerException {
throw ex;
}
});
DOMResult result = new DOMResult();
transformer.transform(new SAXSource(new XMLFilterImpl(getSAXParser().getXMLReader()) {
Locator locator;
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
AttributesImpl resultAtts = new AttributesImpl(atts);
resultAtts.addAttribute(JAXX_INTERNAL_NAMESPACE, "line", "internal:line", "CDATA", String.valueOf(locator.getLineNumber()));
getContentHandler().startElement(uri, localName, qName, resultAtts);
}
}, new InputSource(in)), result);
return (Document) result.getNode();
}
catch (TransformerConfigurationException e) {
throw new RuntimeException(e);
}
catch (TransformerException e) {
Throwable ex = e;
while (ex.getCause() != null)
ex = ex.getCause();
if (ex instanceof IOException)
throw (IOException) ex;
if (ex instanceof SAXException)
throw (SAXException) ex;
if (ex instanceof RuntimeException)
throw (RuntimeException) ex;
throw new RuntimeException(ex);
}
}
public File getBaseDir() {
return baseDir;
}
public Set/*<String>*/ getImportedClasses() {
return importedClasses;
}
public Set/*<String>*/ getImportedPackages() {
return importedPackages;
}
private boolean inlineCreation(CompiledObject object) {
return object.getId().startsWith("$") && object.getInitializationCode(this).length() < INLINE_THRESHOLD;
}
public void checkOverride(CompiledObject object) throws CompilerException {
if (object.getId().startsWith("$"))
return;
ClassDescriptor ancestor = root.getObjectClass();
if (ancestor == object.getObjectClass())
return;
while (ancestor != null) {
try {
FieldDescriptor f = ancestor.getDeclaredFieldDescriptor(object.getId());
if (!f.getType().isAssignableFrom(object.getObjectClass()))
reportError("attempting to redefine superclass member '" + object.getId() + "' as incompatible type (was " + f.getType() + ", redefined as " + object.getObjectClass() + ")");
object.setOverride(true);
break;
}
catch (NoSuchFieldException e) {
ancestor = ancestor.getSuperclass();
}
}
}
private Iterator/*<CompiledObject>*/ getObjectCreationOrder() {
return objects.values().iterator();
}
protected JavaMethod createConstructor(String className) throws CompilerException {
StringBuffer code = new StringBuffer();
String constructorParams = root.getConstructorParams();
if (constructorParams != null) {
code.append(" super(" + constructorParams + ");");
code.append(getLineSeparator());
}
code.append("$initialize();");
code.append(getLineSeparator());
return new JavaMethod(Modifier.PUBLIC, null, className, null, null, code.toString());
}
protected JavaMethod createInitializer(String className) throws CompilerException {
StringBuffer code = new StringBuffer();
code.append("$objectMap.put(" + TypeManager.getJavaCode(root.getId()) + ", this);");
code.append(getLineSeparator());
Iterator/*<CompiledObject>*/ i = getObjectCreationOrder();
boolean lastWasMethodCall = false;
while (i.hasNext()) {
CompiledObject object = (CompiledObject) i.next();
if (object != root && !object.isOverride()) {
if (inlineCreation(object)) {
if (lastWasMethodCall) {
lastWasMethodCall = false;
code.append(getLineSeparator());
}
code.append(getCreationCode(object));
code.append(getLineSeparator());
}
else {
code.append(object.getCreationMethodName() + "();");
code.append(getLineSeparator());
lastWasMethodCall = true;
}
}
}
String rootCode = root.getInitializationCode(this);
if (rootCode != null && rootCode.length() > 0) {
code.append(rootCode);
code.append(getLineSeparator());
}
code.append(getLineSeparator());
if (initializer.length() > 0) {
code.append(initializer);
code.append(getLineSeparator());
}
code.append("$completeSetup();");
code.append(getLineSeparator());
return new JavaMethod(Modifier.PRIVATE, "void", "$initialize", null, null, code.toString());
}
protected JavaMethod createCompleteSetupMethod() {
StringBuffer code = new StringBuffer();
code.append("allComponentsCreated = true;");
code.append(getLineSeparator());
Iterator/*<CompiledObject>*/i = objects.values().iterator();
while (i.hasNext()) {
CompiledObject object = (CompiledObject) i.next();
if (object.getId().startsWith("$"))
code.append(object.getAdditionCode());
else {
code.append(object.getAdditionMethodName() + "();" + getLineSeparator());
String additionCode = object.getAdditionCode();
if (additionCode.length() > 0)
additionCode = "if (allComponentsCreated) {" + getLineSeparator() + additionCode + "}";
javaFile.addMethod(new JavaMethod(Modifier.PROTECTED, "void", object.getAdditionMethodName(), null, null, additionCode));
}
code.append(getLineSeparator());
}
code.append(initDataBindings);
if (lateInitializer.length() > 0) {
code.append(lateInitializer);
code.append(getLineSeparator());
}
return new JavaMethod(Modifier.PRIVATE, "void", "$completeSetup", null, null, code.toString());
}
protected JavaMethod createProcessDataBindingMethod() {
StringBuffer code = new StringBuffer();
boolean superclassIsJAXXObject = ClassDescriptorLoader.getClassDescriptor(JAXXObject.class).isAssignableFrom(root.getObjectClass());
// the force parameter forces the update to happen even if it is already in activeBindings. This
// is used on superclass invocations b/c by the time the call gets to the superclass, it is already
// marked active and would otherwise be skipped
if (processDataBinding.length() > 0) {
code.append(" if (!$force && $activeBindings.contains($dest)) return;");
code.append(getLineSeparator());
code.append(" $activeBindings.add($dest);");
code.append(getLineSeparator());
code.append(" try {");
code.append(getLineSeparator());
if (processDataBinding.length() > 0) {
code.append(processDataBinding);
code.append(getLineSeparator());
}
if (superclassIsJAXXObject) {
code.append(" else");
code.append(getLineSeparator());
code.append(" super.processDataBinding($dest, true);");
code.append(getLineSeparator());
}
code.append(" }");
code.append(getLineSeparator());
code.append(" finally {");
code.append(getLineSeparator());
code.append(" $activeBindings.remove($dest);");
code.append(getLineSeparator());
code.append(" }");
code.append(getLineSeparator());
}
else if (superclassIsJAXXObject) {
code.append(" super.processDataBinding($dest, true);");
code.append(getLineSeparator());
}
return new JavaMethod(Modifier.PUBLIC, "void", "processDataBinding",
new JavaArgument[] { new JavaArgument("String", "$dest"), new JavaArgument("boolean", "$force") },
null, code.toString());
}
protected void createJavaFile(String packageName, String className) throws CompilerException{
String fullClassName = packageName != null ? packageName + "." + className : className;
if (root == null)
throw new CompilerException("root tag must be a class tag");
ClassDescriptor superclass = root.getObjectClass();
boolean superclassIsJAXXObject = ClassDescriptorLoader.getClassDescriptor(JAXXObject.class).isAssignableFrom(superclass);
javaFile.setModifiers(Modifier.PUBLIC);
javaFile.setClassName(fullClassName);
javaFile.setSuperClass(getCanonicalName(superclass));
javaFile.setInterfaces(new String[] { getCanonicalName(JAXXObject.class) });
Iterator/*<CompiledObject>*/ i = objects.values().iterator();
while (i.hasNext()) {
CompiledObject object = (CompiledObject) i.next();
if (!object.isOverride() && !(object instanceof ScriptInitializer)) {
int access = object.getId().startsWith("$") ? Modifier.PRIVATE : Modifier.PROTECTED;
if (object == root)
javaFile.addField(new JavaField(access, fullClassName, object.getId(), "this"));
else
javaFile.addField(new JavaField(access, getCanonicalName(object.getObjectClass()), object.getId()));
}
}
if (!superclassIsJAXXObject) {
javaFile.addField(new JavaField(Modifier.PROTECTED, "java.util.List", "$activeBindings", "new ArrayList()"));
javaFile.addField(new JavaField(Modifier.PROTECTED, "java.util.Map", "$bindingSources", "new HashMap()"));
}
if (stylesheet != null)
javaFile.addField(new JavaField(0, "java.util.Map", "$previousValues", "new java.util.HashMap()"));
javaFile.addMethod(createConstructor(className));
javaFile.addMethod(createInitializer(className));
for (int j = 0; j < dataBindings.size(); j++) {
DataBinding dataBinding = (DataBinding) dataBindings.get(j);
if (dataBinding.compile(true))
initDataBindings.append("applyDataBinding(" + TypeManager.getJavaCode(dataBinding.getId()) + ");" + JAXXCompiler.getLineSeparator());
}
javaFile.addBodyCode(bodyCode.toString());
i = objects.values().iterator();
while (i.hasNext()) {
CompiledObject object = (CompiledObject) i.next();
if (!inlineCreation(object)) {
if (object != root)
javaFile.addMethod(new JavaMethod(Modifier.PROTECTED, "void", object.getCreationMethodName(), null, null, getCreationCode(object)));
}
}
javaFile.addField(new JavaField(Modifier.PRIVATE, "boolean", "allComponentsCreated"));
javaFile.addMethod(createCompleteSetupMethod());
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "applyDataBinding", new JavaArgument[] { new JavaArgument("String", "$binding") },
null, applyDataBinding.toString() + getLineSeparator() + " processDataBinding($binding);"));
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "removeDataBinding", new JavaArgument[] { new JavaArgument("String", "$binding") },
null, removeDataBinding.toString()));
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "processDataBinding", new JavaArgument[] { new JavaArgument("String", "dest") },
null, "processDataBinding(dest, false);"));
javaFile.addMethod(createProcessDataBindingMethod());
if (!superclassIsJAXXObject) {
javaFile.addField(createObjectMap());
javaFile.addMethod(createGetObjectByIdMethod());
}
javaFile.addField(createJAXXObjectDescriptorField());
javaFile.addMethod(createGetJAXXObjectDescriptorMethod());
ClassDescriptor currentClass = root.getObjectClass();
MethodDescriptor firePropertyChange = null;
while (firePropertyChange == null && currentClass != null) {
try {
firePropertyChange = currentClass.getDeclaredMethodDescriptor("firePropertyChange", new ClassDescriptor[] {
ClassDescriptorLoader.getClassDescriptor(String.class),
ClassDescriptorLoader.getClassDescriptor(Object.class),
ClassDescriptorLoader.getClassDescriptor(Object.class)
});
}
catch (NoSuchMethodException e) {
currentClass = currentClass.getSuperclass();
}
}
int modifiers = firePropertyChange != null ? firePropertyChange.getModifiers() : 0;
if (Modifier.isPublic(modifiers)) {
// we have all the support we need
}
if (Modifier.isProtected(modifiers)) {
// there is property change support but the firePropertyChange method is protected
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "firePropertyChange", new JavaArgument[] {
new JavaArgument("java.lang.String", "propertyName"), new JavaArgument("java.lang.Object", "oldValue"), new JavaArgument("java.lang.Object", "newValue") },
null, "super.firePropertyChange(propertyName, oldValue, newValue);"));
}
else {
// either no support at all or firePropertyChange isn't accessible
addPropertyChangeSupport(javaFile);
}
addEventHandlers(javaFile);
if (ClassDescriptorLoader.getClassDescriptor(Application.class).isAssignableFrom(root.getObjectClass()) && !mainDeclared) {
// TODO: check for existing main method first
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC | Modifier.STATIC, "void", "main",
new JavaArgument[] { new JavaArgument("String[]", "arg") }, null,
"SwingUtilities.invokeLater(new Runnable() { public void run() { new " + className + "().setVisible(true); } });"));
}
}
protected void outputClass(String packageName, String className, PrintWriter out) throws CompilerException {
createJavaFile(packageName, className);
out.println(javaFile.toString());
}
public void addImport(String text) {
if (text.endsWith("*"))
importedPackages.add(text.substring(0, text.length() - 1));
else
importedClasses.add(text);
if (!text.equals("*"))
getJavaFile().addImport(text);
}
private JavaField createObjectMap() {
return new JavaField(Modifier.PROTECTED, "Map", "$objectMap", "new HashMap()");
}
protected JavaMethod createGetObjectByIdMethod() {
return new JavaMethod(Modifier.PUBLIC, "java.lang.Object", "getObjectById",
new JavaArgument[] { new JavaArgument("String", "id") }, null,
"return $objectMap.get(id);");
}
public JAXXObjectDescriptor getJAXXObjectDescriptor() {
runInitializers();
CompiledObject[] components = (CompiledObject[]) new ArrayList/*<CompiledObject>*/(objects.values()).toArray(new CompiledObject[objects.size()]);
assert initializers.isEmpty() : "there are pending initializers remaining";
assert root != null : "root object has not been defined";
assert Arrays.asList(components).contains(root) : "root object is not registered";
ComponentDescriptor[] descriptors = new ComponentDescriptor[components.length];
// as we print, sort the array so that component's parents are always before the components themselves
for (int i = 0; i < components.length; i++) {
CompiledObject parent = components[i].getParent();
while (parent != null) {
boolean found = false;
for (int j = i + 1; j < components.length; j++) { // found parent after component, swap them
if (components[j] == parent) {
components[j] = components[i];
components[i] = parent;
found = true;
break;
}
}
if (!found)
break;
parent = components[i].getParent();
}
int parentIndex = -1;
if (parent != null) {
for (int j = 0; j < i; j++) {
if (components[j] == parent) {
parentIndex = j;
break;
}
}
}
descriptors[i] = new ComponentDescriptor(components[i].getId(), components[i] == root ? outputClassName : components[i].getObjectClass().getName(),
components[i].getStyleClass(), parentIndex != -1 ? descriptors[parentIndex] : null);
}
Stylesheet stylesheet = getStylesheet();
if (stylesheet == null)
stylesheet = new Stylesheet();
return new JAXXObjectDescriptor(descriptors, stylesheet);
}
protected JavaField createJAXXObjectDescriptorField() {
try {
JAXXObjectDescriptor descriptor = getJAXXObjectDescriptor();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(new GZIPOutputStream(buffer));
out.writeObject(descriptor);
out.close();
// the use of the weird deprecated constructor is deliberate -- we need to store the data as a String
// in the compiled class file, since byte array initialization is horribly inefficient compared to
// String initialization. So we store the bytes in the String, and we quite explicitly want a 1:1
// mapping between bytes and chars, with the high byte of the char set to zero. We can then safely
// reconstitute the original byte[] at a later date. This is unquestionably an abuse of the String
// type, but if we could efficiently store a byte[] we wouldn't have to do this.
String data = new String(buffer.toByteArray(), 0);
int sizeLimit = 65000; // constant strings are limited to 64K, and I'm not brave enough to push right up to the limit
if (data.length() < sizeLimit)
return new JavaField(Modifier.PRIVATE | Modifier.STATIC, "java.lang.String", "$jaxxObjectDescriptor", TypeManager.getJavaCode(data));
else {
StringBuffer initializer = new StringBuffer();
for (int i = 0; i < data.length(); i += sizeLimit) {
String name = "$jaxxObjectDescriptor" + i;
javaFile.addField(new JavaField(Modifier.PRIVATE | Modifier.STATIC, "java.lang.String", name,
TypeManager.getJavaCode(data.substring(i, Math.min(i + sizeLimit, data.length())))));
if (initializer.length() > 0)
initializer.append(" + ");
initializer.append("String.valueOf(" + name + ")");
}
return new JavaField(Modifier.PRIVATE | Modifier.STATIC, "java.lang.String", "$jaxxObjectDescriptor", initializer.toString());
}
}
catch (IOException e) {
throw new RuntimeException("Internal error: can't-happen error", e);
}
}
protected JavaMethod createGetJAXXObjectDescriptorMethod() {
return new JavaMethod(Modifier.PUBLIC | Modifier.STATIC, "jaxx.runtime.JAXXObjectDescriptor", "$getJAXXObjectDescriptor",
null, null, "return jaxx.runtime.Util.decodeCompressedJAXXObjectDescriptor($jaxxObjectDescriptor);");
}
public String getEventHandlerMethodName(EventHandler handler) {
String result = (String) eventHandlerMethodNames.get(handler);
if (result == null) {
result = "$ev" + eventHandlerMethodNames.size();
eventHandlerMethodNames.put(handler, result);
}
return result;
}
protected void addEventHandlers(JavaFile javaFile) {
Iterator/*Map.Entry<String, Map<ClassDescriptor, List<EventHandler>>>*/ i = eventHandlers.entrySet().iterator();
while (i.hasNext()) { // outer loop is iterating over different objects (well, technically, different Java expressions)
Map.Entry/*<String, Map<ClassDescriptor, List<EventHandler>>*/ e1 = (Map.Entry) i.next();
String expression = (String) e1.getKey();
Iterator/*Map.Entry<ClassDescriptor, List<EventHandler>>*/ j = ((Map) e1.getValue()).entrySet().iterator();
while (j.hasNext()) { // iterate over different types of listeners for this particular object (MouseListener, ComponentListener, etc.)
Map.Entry/*<ClassDescriptor, List<EventHandler>>*/ e2 = (Map.Entry) j.next();
ClassDescriptor listenerClass = (ClassDescriptor) e2.getKey();
Iterator/*<EventHandler>*/ k = ((List) e2.getValue()).iterator();
while (k.hasNext()) { // iterate over individual event handlers of a single type
EventHandler handler = (EventHandler) k.next();
String methodName = getEventHandlerMethodName(handler);
MethodDescriptor listenerMethod = handler.getListenerMethod();
if (listenerMethod.getParameterTypes().length != 1)
throw new CompilerException("Expected event handler " + listenerMethod.getName() + " of class " + handler.getListenerClass() + " to have exactly one argument");
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", methodName,
new JavaArgument[] { new JavaArgument(getCanonicalName(listenerMethod.getParameterTypes()[0]), "event") }, null,
handler.getJavaCode()));
}
}
}
}
protected String getCreationCode(CompiledObject object) throws CompilerException {
if (object instanceof ScriptInitializer)
return object.getInitializationCode(this);
else {
StringBuffer result = new StringBuffer();
result.append(object.getId());
result.append(" = ");
String constructorParams = object.getConstructorParams();
if (constructorParams != null)
result.append("(" + getCanonicalName(object.getObjectClass()) + ") new " + getCanonicalName(object.getObjectClass()) + "(" + constructorParams + ");");
else
result.append("new " + getCanonicalName(object.getObjectClass()) + "();");
result.append(getLineSeparator());
String initCode = object.getInitializationCode(this);
if (initCode != null && initCode.length() > 0)
result.append(initCode);
result.append("$objectMap.put(" + TypeManager.getJavaCode(object.getId()) + ", " + object.getId() + ");");
return result.toString();
}
}
protected void addPropertyChangeSupport(JavaFile javaFile) throws CompilerException {
javaFile.addField(new JavaField(0, "java.beans.PropertyChangeSupport", "$propertyChangeSupport"));
javaFile.addMethod(new JavaMethod(0, "java.beans.PropertyChangeSupport", "$getPropertyChangeSupport", null, null,
"if ($propertyChangeSupport == null)\n" +
" $propertyChangeSupport = new PropertyChangeSupport(this);\n" +
"return $propertyChangeSupport;"));
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "addPropertyChangeListener", new JavaArgument[] {
new JavaArgument("java.beans.PropertyChangeListener", "listener") }, null,
"$getPropertyChangeSupport().addPropertyChangeListener(listener);"));
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "addPropertyChangeListener", new JavaArgument[] {
new JavaArgument("java.lang.String", "property"), new JavaArgument("java.beans.PropertyChangeListener", "listener") }, null,
"$getPropertyChangeSupport().addPropertyChangeListener(property, listener);"));
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "removePropertyChangeListener", new JavaArgument[] {
new JavaArgument("java.beans.PropertyChangeListener", "listener") }, null,
"$getPropertyChangeSupport().removePropertyChangeListener(listener);"));
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "removePropertyChangeListener", new JavaArgument[] {
new JavaArgument("java.lang.String", "property"), new JavaArgument("java.beans.PropertyChangeListener", "listener") }, null,
"$getPropertyChangeSupport().removePropertyChangeListener(property, listener);"));
javaFile.addMethod(new JavaMethod(Modifier.PUBLIC, "void", "firePropertyChange", new JavaArgument[] {
new JavaArgument("java.lang.String", "propertyName"), new JavaArgument("java.lang.Object", "oldValue"), new JavaArgument("java.lang.Object", "newValue") },
null, "$getPropertyChangeSupport().firePropertyChange(propertyName, oldValue, newValue);"));
}
public void compileFirstPass(final Element tag) throws IOException {
tagsBeingCompiled.push(tag);
String namespace = tag.getNamespaceURI();
String fullClassName = null;
String localName = tag.getLocalName();
boolean namespacePrefix = tag.getPrefix() != null;
// resolve class tags into fully-qualified class name
if (namespace != null && namespace.endsWith("*")) {
String packageName = namespace.substring(0, namespace.length() - 1);
if (localName.startsWith(packageName)) // class name is fully-qualified already
fullClassName = TagManager.resolveClassName(localName, this);
else { // namespace not included in class name, probably need the namespace to resolve
fullClassName = TagManager.resolveClassName(packageName + localName, this);
if (fullClassName == null && !namespacePrefix) // it was just a default namespace, try again without using the namespace
fullClassName = TagManager.resolveClassName(localName, this);
}
}
else
fullClassName = TagManager.resolveClassName(localName, this);
if (fullClassName != null) { // we are definitely dealing with a class tag
addDependencyClass(fullClassName);
namespace = fullClassName.substring(0, fullClassName.lastIndexOf(".") + 1) + "*";
if (symbolTable.getSuperclassName() == null)
symbolTable.setSuperclassName(fullClassName);
String id = tag.getAttribute("id");
if (id.length() > 0)
symbolTable.getClassTagIds().put(id, fullClassName);
}
// during the first pass, we can't create ClassDescriptors for JAXX files because they may not have been processed yet
// (and we can't wait until they have been processed because of circular dependencies). So we don't do any processing
// during the first pass which requires having a ClassDescriptor; here we determine whether we have a class tag or not
// (class tag namespaces end in "*") and use a generic handler if so. The real handler is used during the second pass.
TagHandler handler = (namespace != null && namespace.endsWith("*")) ? firstPassClassTagHandler : TagManager.getTagHandler(tag.getNamespaceURI(), localName, namespacePrefix, this);
if (handler != firstPassClassTagHandler && handler instanceof DefaultObjectHandler) {
fullClassName = ((DefaultObjectHandler) handler).getBeanClass().getName();
namespace = fullClassName.substring(0, fullClassName.lastIndexOf(".") + 1) + "*";
handler = firstPassClassTagHandler;
}
if (handler == firstPassClassTagHandler) {
final String finalClassName = fullClassName;
registerInitializer(new Runnable() { // register an initializer which will create the CompiledObject after pass 1
public void run() {
DefaultObjectHandler handler = (DefaultObjectHandler) TagManager.getTagHandler(null, finalClassName, JAXXCompiler.this);
if (handler == null)
throw new CompilerException("Internal error: missing TagHandler for '" + finalClassName + "'");
handler.registerCompiledObject(tag, JAXXCompiler.this);
}
});
}
if (handler != null) {
try {
handler.compileFirstPass(tag, this);
}
catch (CompilerException e) {
reportError(e);
}
}
else {
reportError("Could not find a Java class corresponding to: <" + tag.getTagName() + ">");
failed = true;
}
Element finished = (Element) tagsBeingCompiled.pop();
if (finished != tag)
throw new RuntimeException("internal error: just finished compiling " + tag + ", but top of tagsBeingCompiled stack is " + finished);
}
public void compileSecondPass(Element tag) throws IOException {
tagsBeingCompiled.push(tag);
TagHandler handler = TagManager.getTagHandler(tag.getNamespaceURI(), tag.getLocalName(), tag.getPrefix() != null, this);
if (handler != null)
handler.compileSecondPass(tag, this);
else {
reportError("Could not find a Java class corresponding to: <" + tag.getTagName() + ">");
assert false : "can't-happen error: error should have been reported during the fast pass and caused an abort";
failed = true;
}
Element finished = (Element) tagsBeingCompiled.pop();
if (finished != tag)
throw new RuntimeException("internal error: just finished compiling " + tag + ", but top of tagsBeingCompiled stack is " + finished);
}
// 1.5 adds getCanonicalName; unfortunately we can't depend on 1.5 features yet
public static String getCanonicalName(Class clazz) {
if (clazz.isArray()) {
String canonicalName = getCanonicalName(clazz.getComponentType());
if (canonicalName != null)
return canonicalName + "[]";
else
return null;
}
else
return clazz.getName().replace('$', '.');
}
public static String getCanonicalName(ClassDescriptor clazz) {
if (clazz.isArray()) {
String canonicalName = getCanonicalName(clazz.getComponentType());
if (canonicalName != null)
return canonicalName + "[]";
else
return null;
}
else
return clazz.getName().replace('$', '.');
}
public static String capitalize(String s) {
if (s.length() == 0)
return s;
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
public String[] parseParameterList(String parameters) throws CompilerException {
List/*<String>*/ result = new ArrayList/*<String>*/();
StringBuffer current = new StringBuffer();
int state = 0; // normal
for (int i = 0; i < parameters.length(); i++) {
char c = parameters.charAt(i);
switch (state) {
case 0: // normal
switch (c) {
case '"': current.append(c); state = 1; break; // in quoted string
case '\\': current.append(c); state = 2; break; // immediately after backslash
case ',': if (current.length() > 0) {
result.add(current.toString());
current.setLength(0);
break;
}
else
reportError("error parsing parameter list: " + parameters);
default: current.append(c);
}
break;
case 1: // in quoted string
switch (c) {
case '"': current.append(c); state = 0; break; // normal
case '\\': current.append(c); state = 3; break; // immediate after backslash in quoted string
default: current.append(c);
}
break;
case 2: // immediately after backslash
current.append(c);
state = 0; // normal
break;
case 3: // immediately after backslash in quoted string
current.append(c);
state = 1; // in quoted string
break;
}
}
if (current.length() > 0)
result.add(current.toString());
return (String[]) result.toArray(new String[result.size()]);
}
public void openComponent(CompiledObject component) throws CompilerException {
openComponent(component, null);
}
public void openComponent(CompiledObject component, String constraints) throws CompilerException {
CompiledObject parent = getOpenComponent();
openInvisibleComponent(component);
if (parent != null && !component.isOverride())
parent.addChild(component, constraints, this);
}
public void openInvisibleComponent(CompiledObject component) {
if (!ids.containsKey(component))
registerCompiledObject(component);
openComponents.push(component);
}
public CompiledObject getOpenComponent() {
if (openComponents.isEmpty())
return null;
else
return (CompiledObject) openComponents.peek();
}
public void closeComponent(CompiledObject component) {
if (openComponents.pop() != component)
throw new IllegalArgumentException("can only close the topmost open object");
}
public CompiledObject getRootObject() {
return root;
}
public void registerCompiledObject(CompiledObject object) {
assert symbolTables.values().contains(symbolTable) : "attempting to register CompiledObject before pass 1 is complete";
if (root == null)
root = object;
String id = object.getId();
if (ids.containsKey(object))
reportError("object '" + object + "' is already registered with id '" + ids.get(object) + "', cannot re-register as '" + id + "'");
if (objects.containsKey(id) && !(objects.get(id) instanceof Element))
reportError("id '" + id + "' is already registered to component " + objects.get(id));
objects.put(id, object);
ids.put(object, id);
}
public String getAutoId(ClassDescriptor objectClass) {
if (options.getOptimize()) {
return "$" + Integer.toString(autogenID++, 36);
}
else {
String name = objectClass.getName();
name = name.substring(name.lastIndexOf(".") + 1);
return "$" + name + autogenID++;
}
}
public String getUniqueId(Object object) {
String result = (String) uniqueIds.get(object);
if (result == null) {
result = "$u" + uniqueIds.size();
uniqueIds.put(object, result);
}
return result;
}
public SymbolTable getSymbolTable() {
return symbolTable;
}
public CompiledObject getCompiledObject(String id) {
runInitializers();
assert symbolTables.values().contains(symbolTable) : "attempting to retrieve CompiledObject before pass 1 is complete";
return (CompiledObject) objects.get(id);
}
private Matcher leftBraceMatcher = Pattern.compile("^(\\{)|[^\\\\](\\{)").matcher("");
private int getNextLeftBrace(String string, int pos) {
leftBraceMatcher.reset(string);
return leftBraceMatcher.find(pos) ? Math.max(leftBraceMatcher.start(1), leftBraceMatcher.start(2)) : -1;
}
private Matcher rightBraceMatcher = Pattern.compile("^(\\})|[^\\\\](\\})").matcher("");
private int getNextRightBrace(String string, int pos) {
leftBraceMatcher.reset(string);
rightBraceMatcher.reset(string);
int openCount = 1;
int rightPos = -1;
while (openCount > 0) {
pos++;
int leftPos = leftBraceMatcher.find(pos) ? Math.max(leftBraceMatcher.start(1), leftBraceMatcher.start(2)) : -1;
rightPos = rightBraceMatcher.find(pos) ? Math.max(rightBraceMatcher.start(1), rightBraceMatcher.start(2)) : -1;
assert leftPos == -1 || leftPos >= pos;
assert rightPos == -1 || rightPos >= pos;
if (leftPos != -1 && leftPos < rightPos) {
pos = leftPos;
openCount++;
}
else if (rightPos != -1) {
pos = rightPos;
openCount--;
}
else
openCount = 0;
}
return pos;
}
/** Examine an attribute value for data binding expressions. Returns a 'cooked' expression which
* can be used to determine the resulting value. It is expected that this expression will be used
* as the source expression in a call to {@link #registerDataBinding}.
* If the attribute value does not invoke data binding, this method returns <code>null</code>
*
*@param stringValue the string value of the property from the XML
*@param type the type of the property, from the <code>JAXXPropertyDescriptor</code>
*@return a processed version of the expression
*/
public String processDataBindings(String stringValue, ClassDescriptor type) throws CompilerException {
int pos = getNextLeftBrace(stringValue, 0);
if (pos != -1) {
StringBuffer expression = new StringBuffer();
int lastPos = 0;
while (pos != -1 && pos < stringValue.length()) {
if (pos > lastPos) {
if (expression.length() > 0)
expression.append(" + ");
expression.append('"');
expression.append(JAXXCompiler.escapeJavaString(stringValue.substring(lastPos, pos)));
expression.append('"');
}
if (expression.length() > 0)
expression.append(" + ");
expression.append('(');
int pos2 = getNextRightBrace(stringValue, pos + 1);
if (pos2 == -1) {
reportError("unmatched '{' in expression: " + stringValue);
return "";
}
expression.append(stringValue.substring(pos + 1, pos2));
expression.append(')');
pos2++;
if (pos2 < stringValue.length()) {
pos = getNextLeftBrace(stringValue, pos2);
lastPos = pos2;
}
else {
pos = stringValue.length();
lastPos = pos;
}
}
if (lastPos < stringValue.length()) {
if (expression.length() > 0)
expression.append(" + ");
expression.append('"');
expression.append(JAXXCompiler.escapeJavaString(stringValue.substring(lastPos)));
expression.append('"');
}
return type == ClassDescriptorLoader.getClassDescriptor(String.class) ? "String.valueOf(" + expression + ")" : expression.toString();
}
return null;
}
public void registerDataBinding(String src, String dest, String assignment) {
try {
src = checkJavaCode(src);
dataBindings.add(new DataBinding(src, dest, assignment, this));
}
catch (CompilerException e) {
reportError("While parsing data binding for '" + dest.substring(dest.lastIndexOf(".") + 1) + "': " + e.getMessage());
}
}
public ScriptManager getScriptManager() {
return scriptManager;
}
/** Verifies that a snippet of Java code parses correctly. A warning is generated if the string has enclosing
* curly braces. Returns a "cooked" version of the string which has enclosing curly braces removed.
*
*@param javaCode the Java code snippet to test
*@throws CompilerException if the code cannot be parsed
*/
public String checkJavaCode(String javaCode) {
javaCode = scriptManager.trimScript(javaCode);
scriptManager.checkParse(javaCode);
return javaCode;
}
public void registerEventHandler(EventHandler handler) {
String objectCode = handler.getObjectCode();
Map/*<ClassDescriptor, List<EventHandler>>*/ listeners = (Map) eventHandlers.get(objectCode);
if (listeners == null) {
listeners = new HashMap/*<ClassDescriptor, List<EventHandler>>*/();
eventHandlers.put(objectCode, listeners);
}
ClassDescriptor listenerClass = handler.getListenerClass();
List/*<EventHandler>*/ handlerList = (List) listeners.get(listenerClass);
if (handlerList == null) {
handlerList = new ArrayList/*<EventHandler>*/();
listeners.put(listenerClass, handlerList);
}
handlerList.add(handler);
}
public FieldDescriptor[] getScriptFields() {
List/*<FieldDescriptor>*/ scriptFields = symbolTable.getScriptFields();
return (FieldDescriptor[]) scriptFields.toArray(new FieldDescriptor[scriptFields.size()]);
}
public void addScriptField(FieldDescriptor field) {
symbolTable.getScriptFields().add(field);
}
public MethodDescriptor[] getScriptMethods() {
List/*<MethodDescriptor>*/ scriptMethods = symbolTable.getScriptMethods();
return (MethodDescriptor[]) scriptMethods.toArray(new MethodDescriptor[scriptMethods.size()]);
}
public void addScriptMethod(MethodDescriptor method) {
if (method.getName().equals("main") && method.getParameterTypes().length == 1 && method.getParameterTypes()[0].getName().equals("[Ljava.lang.String;"))
mainDeclared = true;
symbolTable.getScriptMethods().add(method);
}
public void registerScript(String script) throws CompilerException {
registerScript(script, null);
}
public void registerScript(String script, File sourceFile) throws CompilerException {
if (sourceFile != null)
sourceFiles.push(sourceFile);
scriptManager.registerScript(script);
if (sourceFile != null) {
File pop = (File) sourceFiles.pop();
if (pop != sourceFile)
throw new RuntimeException("leaving registerScript(), but " + sourceFile + " was not the top entry on the stack (found " + pop + " instead)");
}
}
public String preprocessScript(String script) throws CompilerException {
return scriptManager.preprocessScript(script);
}
public void registerStylesheet(Stylesheet stylesheet) {
if (this.stylesheet == null)
this.stylesheet = stylesheet;
else
this.stylesheet.add(stylesheet.getRules());
}
public Stylesheet getStylesheet() {
Stylesheet merged = new Stylesheet();
if (stylesheet != null)
merged.add(stylesheet.getRules());
merged.add((Rule[]) inlineStyles.toArray(new Rule[inlineStyles.size()]));
return merged;
}
public Stack/*<File>*/ getSourceFiles() {
return sourceFiles;
}
public void addInlineStyle(CompiledObject object, String propertyName, boolean dataBinding) {
inlineStyles.add(Rule.inlineAttribute(object, propertyName, dataBinding));
}
public void reportWarning(String warning) {
Element currentTag = null;
if (!tagsBeingCompiled.isEmpty())
currentTag = (Element) tagsBeingCompiled.peek();
reportWarning(currentTag, warning, 0);
}
public void reportWarning(Element tag, String warning, int lineOffset) {
String lineNumber = null;
if (tag != null) {
String lineAttr = tag.getAttributeNS(JAXX_INTERNAL_NAMESPACE, "line");
if (lineAttr.length() > 0)
lineNumber = lineAttr;
}
File src = (File) sourceFiles.peek();
try {
src = src.getCanonicalFile();
}
catch (IOException e) {
}
System.err.print(src);
if (lineNumber != null)
System.err.print(":" + ((sourceFiles.size() == 1) ? Integer.parseInt(lineNumber) + lineOffset : lineOffset + 1));
System.err.println(": Warning: " + warning);
warningCount++;
}
public void reportError(String error) {
Element currentTag = null;
if (!tagsBeingCompiled.isEmpty())
currentTag = (Element) tagsBeingCompiled.peek();
reportError(currentTag, error);
}
public void reportError(CompilerException ex) {
reportError(null, ex);
}
public void reportError(String extraMessage, CompilerException ex) {
String message = ex.getMessage();
if (ex.getClass() == UnsupportedAttributeException.class || ex.getClass() == UnsupportedTagException.class)
message = ex.getClass().getName().substring(ex.getClass().getName().lastIndexOf(".") + 1) + ": " + message;
int lineOffset;
if (ex instanceof ParseException)
lineOffset = Math.max(0, ((ParseException) ex).getLine() - 1);
else
lineOffset = 0;
Element currentTag = null;
if (!tagsBeingCompiled.isEmpty())
currentTag = (Element) tagsBeingCompiled.peek();
reportError(currentTag, extraMessage != null ? extraMessage + message : message, lineOffset);
}
public void reportError(Element tag, String error) {
reportError(tag, error, 0);
}
public void reportError(Element tag, String error, int lineOffset) {
int lineNumber = 0;
if (tag != null) {
String lineAttr = tag.getAttributeNS(JAXX_INTERNAL_NAMESPACE, "line");
if (lineAttr.length() > 0)
lineNumber = Integer.parseInt(lineAttr);
}
lineNumber = Math.max(lineNumber, 1) + lineOffset;
reportError(lineNumber, error);
}
public void reportError(int lineNumber, String error) {
File src = sourceFiles.isEmpty() ? null : (File) sourceFiles.peek();
try {
if (src != null)
src = src.getCanonicalFile();
}
catch (IOException e) {
}
System.err.print(src != null ? src.getPath() : "<unknown source>");
if (lineNumber > 0)
System.err.print(":" + lineNumber);
System.err.println(": " + error);
errorCount++;
failed = true;
}
/** Escapes a string using standard Java escape sequences, generally in preparation to including it in a string literal
* in a compiled Java file.
*
*@param raw the raw string to be escape
*@return a string in which all 'dangerous' characters have been replaced by equivalent Java escape sequences
**/
public static String escapeJavaString(String raw) {
StringBuffer out = new StringBuffer(raw);
for (int i = 0; i < out.length(); i++) {
char c = out.charAt(i);
if (c == '\\' || c == '"') {
out.insert(i, '\\');
i++;
}
else if (c == '\n') {
out.replace(i, i + 1, "\\n");
i++;
}
else if (c == '\r') {
out.replace(i, i + 1, "\\r");
i++;
}
else if (c < 32 || c > 127) {
String value = Integer.toString((int) c, 16);
while (value.length() < 4)
value = "0" + value;
out.replace(i, i + 1, "\\u" + value);
i += 5;
}
}
return out.toString();
}
/** Returns the system line separator string.
*
*@return the string used to separate lines
*/
public static String getLineSeparator() {
return System.getProperty("line.separator", "\n");
}
/** Returns a <code>ClassLoader</code> which searches the user-specified class path in addition
* to the normal system class path.
*
*@return <code>ClassLoader</code> to use while resolving class references
*/
public ClassLoader getClassLoader() {
if (classLoader == null) {
String classPath = options.getClassPath();
if (classPath == null)
classPath = ".";
String[] paths = classPath.split(File.pathSeparator);
URL[] urls = new URL[paths.length];
for (int i = 0; i < paths.length; i++) {
try {
urls[i] = new File(paths[i]).toURL();
}
catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
classLoader = new URLClassLoader(urls, getClass().getClassLoader());
}
return classLoader;
}
/** Returns the compiler instance which is processing the specified JAXX class. Each class is compiled by a
* different compiler instance.
*/
public static JAXXCompiler getJAXXCompiler(String className) {
return compilers != null ? (JAXXCompiler) compilers.get(className) : null;
}
/** Returns the symbol table for the specified JAXX class. Must be called during the second compiler pass.
* Returns <code>null</code> if no such symbol table could be found.
*/
public static SymbolTable getSymbolTable(String className) {
JAXXCompiler compiler = getJAXXCompiler(className);
if (compiler == null)
return null;
return compiler.getSymbolTable();
}
public static File URLtoFile(URL url) {
return URLtoFile(url.toString());
}
public static File URLtoFile(String urlString) {
if (!urlString.startsWith("file:"))
throw new IllegalArgumentException("url must start with 'file:'");
urlString = urlString.substring("file:".length());
if (urlString.startsWith("/") && System.getProperty("os.name").startsWith("Windows"))
urlString = urlString.substring(1);
try {
return new File(URLDecoder.decode(urlString.replace('/', File.separatorChar), "utf-8"));
}
catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public void addDependencyClass(String className) {
if (!jaxxFileClassNames.contains(className)) {
URL jaxxURL = getClassLoader().getResource(className.replace('.', '/') + ".jaxx");
URL classURL = getClassLoader().getResource(className.replace('.', '/') + ".class");
if (jaxxURL != null && classURL != null) {
try {
File jaxxFile = URLtoFile(jaxxURL);
File classFile = URLtoFile(classURL);
if (classFile.lastModified() > jaxxFile.lastModified())
return; // class file is newer, no need to recompile
}
catch (Exception e) {
// do nothing
}
}
if (jaxxURL != null && jaxxURL.toString().startsWith("file:")) {
File jaxxFile = URLtoFile(jaxxURL);
try {
jaxxFile = jaxxFile.getCanonicalFile();
}
catch (IOException ex) {
}
assert jaxxFile.getName().equalsIgnoreCase(className.substring(className.lastIndexOf(".") + 1) + ".jaxx") :
"expecting file name to match " + className + ", but found " + jaxxFile.getName();
if (jaxxFile.getName().equals(className.substring(className.lastIndexOf(".") + 1) + ".jaxx")) { // check case match
if (currentPass == PASS_2)
throw new AssertionError("Internal error: adding dependency class " + className + " during second compilation pass");
jaxxFileClassNames.add(className);
jaxxFiles.add(jaxxFile);
}
else
return; // case mismatch, ignore
}
}
}
/** Compiled a set of files, expressed as paths relative to a base directory. The class names of the compiled files are derived
* from the relative path strings (e.g. "example/Foo.jaxx" compiles into a class named "example.Foo"). Returns <code>true</code>
* if compilation succeeds, <code>false</code> if it fails. Warning and error messages are sent to <code>System.err</code>.
*
*@param base the directory against which to resolve relative paths
*@param relativePaths a list of relative paths to .jaxx files being compiled
*@param options the compiler options to use
*@return <code>true</code> if compilation succeeds, <code>false</code> otherwise
*/
public static synchronized boolean compile(File base, String[] relativePaths, CompilerOptions options) {
File[] files = new File[relativePaths.length];
String[] classNames = new String[relativePaths.length];
for (int i = 0; i < files.length; i++) {
files[i] = new File(base, relativePaths[i]);
classNames[i] = relativePaths[i].substring(0, relativePaths[i].lastIndexOf("."));
classNames[i] = classNames[i].replace(File.separatorChar, '.');
classNames[i] = classNames[i].replace('/', '.');
classNames[i] = classNames[i].replace('\\', '.');
classNames[i] = classNames[i].replace(':', '.');
}
return compile(files, classNames, options);
}
/** Resets all state in preparation for a new compilation session. */
private static void reset() {
errorCount = 0;
warningCount = 0;
jaxxFiles.clear();
jaxxFileClassNames.clear();
symbolTables.clear();
compilers.clear();
}
/** Compiled a set of files, with the class names specified explicitly. The class compiled from files[i] will be named classNames[i].
* Returns <code>true</code> if compilation succeeds, <code>false</code> if it fails. Warning and error messages are sent to
* <code>System.err</code>.
*
*@param files the .jaxx files to compile
*@param classNames the names of the classes being compiled
*@param options the compiler options to use
*@return <code>true</code> if compilation succeeds, <code>false</code> otherwise
*/
public static synchronized boolean compile(File[] files, String[] classNames, CompilerOptions options) {
reset(); // just to be safe...
jaxxFiles.addAll(Arrays.asList(files));
jaxxFileClassNames.addAll(Arrays.asList(classNames));
try {
boolean success = true;
// pass 1
currentPass = PASS_1;
boolean compiled;
do {
compiled = false;
assert jaxxFiles.size() == jaxxFileClassNames.size();
Iterator/*<String>*/ filesIterator = new ArrayList/*<File>*/(jaxxFiles).iterator(); // clone it so it can safely be modified while we're iterating
Iterator/*<String>*/ classNamesIterator = new ArrayList/*<String>*/(jaxxFileClassNames).iterator();
while (filesIterator.hasNext()) {
File file = (File) filesIterator.next();
String className = (String) classNamesIterator.next();
if (symbolTables.get(file) == null) {
compiled = true;
if (compilers.containsKey(className))
throw new CompilerException("Internal error: " + className + " is already being compiled, attempting to compile it again");
File destDir = options.getTargetDirectory();
if (destDir != null) {
int dotPos = className.lastIndexOf(".");
if (dotPos != -1)
destDir = new File(destDir, className.substring(0, dotPos).replace('.', File.separatorChar));
destDir.mkdirs();
}
else
destDir = file.getParentFile();
JAXXCompiler compiler = new JAXXCompiler(file.getParentFile(), file, className, options);
compilers.put(className, compiler);
compiler.compileFirstPass();
assert !symbolTables.values().contains(compiler.getSymbolTable()) : "symbolTable is already registered";
symbolTables.put(file, compiler.getSymbolTable());
if (compiler.failed)
success = false;
}
}
}
while (compiled);
// pass 2
currentPass = PASS_2;
if (success) {
assert jaxxFiles.size() == jaxxFileClassNames.size();
List/*<String>*/ jaxxFilesClone = new ArrayList(jaxxFiles);
Iterator/*<String>*/ filesIterator = jaxxFilesClone.iterator();
Iterator/*<String>*/ classNamesIterator = jaxxFileClassNames.iterator();
while (filesIterator.hasNext()) {
File file = (File) filesIterator.next();
String className = (String) classNamesIterator.next();
JAXXCompiler compiler = (JAXXCompiler) compilers.get(className);
if (compiler == null)
throw new CompilerException("Internal error: could not find compiler for " + className + " during second pass");
if (!compiler.failed)
compiler.runInitializers();
compiler.compileSecondPass();
if (compiler.failed)
success = false;
}
if (!jaxxFilesClone.equals(jaxxFiles))
throw new AssertionError("Internal error: compilation set altered during pass 2 (was " + jaxxFilesClone + ", modified to " + jaxxFiles + ")");
}
// stylesheet application
if (success) {
assert jaxxFiles.size() == jaxxFileClassNames.size();
Iterator/*<String>*/ filesIterator = jaxxFiles.iterator();
Iterator/*<String>*/ classNamesIterator = jaxxFileClassNames.iterator();
while (filesIterator.hasNext()) {
File file = (File) filesIterator.next();
String className = (String) classNamesIterator.next();
JAXXCompiler compiler = (JAXXCompiler) compilers.get(className);
if (compiler == null)
throw new CompilerException("Internal error: could not find compiler for " + className + " during stylesheet application");
compiler.applyStylesheets();
if (compiler.failed)
success = false;
}
}
// code generation
if (success) {
assert jaxxFiles.size() == jaxxFileClassNames.size();
Iterator/*<String>*/ filesIterator = jaxxFiles.iterator();
Iterator/*<String>*/ classNamesIterator = jaxxFileClassNames.iterator();
while (filesIterator.hasNext()) {
File file = (File) filesIterator.next();
String className = (String) classNamesIterator.next();
JAXXCompiler compiler = (JAXXCompiler) compilers.get(className);
if (compiler == null)
throw new CompilerException("Internal error: could not find compiler for " + className + " during code generation");
compiler.generateCode();
if (compiler.failed)
success = false;
}
}
// javac
if (success && options.getRunJavac()) {
assert jaxxFiles.size() == jaxxFileClassNames.size();
Iterator/*<String>*/ filesIterator = jaxxFiles.iterator();
Iterator/*<String>*/ classNamesIterator = jaxxFileClassNames.iterator();
while (filesIterator.hasNext()) {
File file = (File) filesIterator.next();
String className = (String) classNamesIterator.next();
JAXXCompiler compiler = (JAXXCompiler) compilers.get(className);
if (compiler == null)
throw new CompilerException("Internal error: could not find compiler for " + className + " during compilation");
compiler.runJavac();
if (compiler.failed)
success = false;
}
}
if (warningCount == 1)
System.err.println("1 warning");
else if (warningCount > 0)
System.err.println(warningCount + " warnings");
if (errorCount == 1)
System.err.println("1 error");
else if (errorCount > 0)
System.err.println(errorCount + " errors");
return success;
}
catch (CompilerException e) {
System.err.println(e.getMessage());
e.printStackTrace();
return false;
}
catch (Throwable e) {
e.printStackTrace();
return false;
}
finally {
reset();
}
}
private static void showUsage() {
System.out.println("Usage: jaxxc <options> <source files>");
System.out.println();
System.out.println("Source files must end in extension .jaxx");
System.out.println("Use JAXX_OPTS environment variable to pass arguments to Java runtime");
System.out.println();
System.out.println("Supported options include:");
System.out.println(" -classpath <paths> paths to search for user classes");
System.out.println(" -cp <paths> same as -classpath");
System.out.println(" -d <directory> target directory for generated class files");
System.out.println(" -javac_opts <opts> options to pass to javac");
System.out.println(" -java or -j produce .java files, but do not compile them");
System.out.println(" -keep or -k preserve generated .java files after compilation");
System.out.println(" -optimize or -o optimize during compilation");
System.out.println(" -version display version information");
System.out.println();
System.out.println("See http://www.jaxxframework.org/ for full documentation.");
}
public static String getVersion() {
return "1.0.3-beta2";
}
public static void main(String[] arg) throws Exception {
boolean success = true;
CompilerOptions options = new CompilerOptions();
List/*<String>*/ files = new ArrayList/*<String>*/();
for (int i = 0; i < arg.length; i++) {
if (arg[i].endsWith(".jaxx")) {
files.add(arg[i]);
}
else if (arg[i].equals("-d")) {
if (++i < arg.length) {
File targetDirectory = new File(arg[i]);
if (!targetDirectory.exists()) {
System.err.println("Error: could not find target directory: " + targetDirectory);
errorCount++;
success = false;
}
options.setTargetDirectory(targetDirectory);
}
else
success = false;
}
else if (arg[i].equals("-cp") || arg[i].equals("-classpath")) {
if (++i < arg.length)
options.setClassPath(arg[i]);
else
success = false;
}
else if (arg[i].equals("-javac_opts")) {
if (++i < arg.length)
options.setJavacOpts(arg[i]);
else
success = false;
}
else if (arg[i].equals("-k") || arg[i].equals("-keep"))
options.setKeepJavaFiles(true);
else if (arg[i].equals("-j") || arg[i].equals("-java")) {
options.setKeepJavaFiles(true);
options.setRunJavac(false);
}
else if (arg[i].equals("-o") || arg[i].equals("-optimize"))
options.setOptimize(true);
else if (arg[i].equals("-version")) {
System.err.println("jaxxc version " + getVersion() + " by Ethan Nicholas");
System.err.println("http://www.jaxxframework.org/");
System.exit(0);
}
else if (arg[i].equals("-internalDumpVersion")) { // used by ant to extract the version info
System.out.println("jaxx.version=" + getVersion());
return;
}
else {
success = false;
}
}
success &= (errorCount == 0 && files.size() > 0);
if (success)
success = compile(new File("."), (String[]) files.toArray(new String[files.size()]), options);
else {
showUsage();
System.exit(1);
}
System.exit(success ? 0 : 1);
}
}
|