JAXXCompiler.java :  » XML-UI » JAXX » jaxx » compiler » Java Open Source

Java Open Source » XML UI » JAXX 
JAXX » jaxx » compiler » JAXXCompiler.java
/*
 * 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);
    }
}                                                                                                                           
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.