]> source.dussan.org Git - aspectj.git/commitdiff
updated tiny XML parser. Unfortunately, also reformatted.
authorwisberg <wisberg>
Wed, 11 May 2005 08:43:26 +0000 (08:43 +0000)
committerwisberg <wisberg>
Wed, 11 May 2005 08:43:26 +0000 (08:43 +0000)
build/src/org/aspectj/internal/tools/build/Module.java

index b3038881945ed07b321f78d468e273ffd7a84837..8539e6c8c514e818a00ba302c1d1d7b1d3b22c6f 100644 (file)
  *     Xerox/PARC     initial implementation 
  * ******************************************************************/
 
 package org.aspectj.internal.tools.build;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileReader;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -28,51 +29,46 @@ import java.util.Properties;
 import java.util.StringTokenizer;
 
 /**
- * This represents an (eclipse) build module/unit 
- * used by a Builder to compile classes 
- * and/or assemble zip file
- * of classes, optionally with all antecedants.
- * This implementation infers attributes from two
- * files in the module directory:
+ * This represents an (eclipse) build module/unit used by a Builder to compile
+ * classes and/or assemble zip file of classes, optionally with all antecedants.
+ * This implementation infers attributes from two files in the module directory:
  * <ul>
- * <li>an Eclipse project <code>.classpath</code> file
- *     containing required libraries and modules
- *     (collectively, "antecedants")
- *     </li>
- * <li>a file <code>{moduleName}.mf.txt</code> is taken as
- *     the manifest of any .jar file produced, after filtering.
- *     </li>
+ * <li>an Eclipse project <code>.classpath</code> file containing required
+ * libraries and modules (collectively, "antecedants") </li>
+ * <li>a file <code>{moduleName}.mf.txt</code> is taken as the manifest of
+ * any .jar file produced, after filtering. </li>
  * </ul>
  * 
  * @see Builder
  * @see Modules#getModule(String)
  */
 public class Module {
-    private static final String[] ATTS = new String[]
-        { "exported", "kind", "path", "sourcepath" };
-        
+    private static final String[] ATTS = new String[] { "exported", "kind",
+            "path", "sourcepath" };
+
     private static final int getATTSIndex(String key) {
         for (int i = 0; i < ATTS.length; i++) {
-            if (ATTS[i].equals(key)) return i;
+            if (ATTS[i].equals(key))
+                return i;
         }
         return -1;
     }
-    /** @return true if file is null or cannot be read or was
-     *           last modified after time
+
+    /**
+     * @return true if file is null or cannot be read or was last modified after
+     *         time
      */
     private static boolean outOfDate(long time, File file) {
-        return ((null == file) 
-            || !file.canRead()
-            || (file.lastModified() > time));
+        return ((null == file) || !file.canRead() || (file.lastModified() > time));
     }
-    
+
     /** @return all source files under srcDir */
     private static Iterator sourceFiles(File srcDir) {
         ArrayList result = new ArrayList();
         sourceFiles(srcDir, result);
         return result.iterator();
     }
-    
+
     private static void sourceFiles(File srcDir, List result) {
         if ((null == srcDir) || !srcDir.canRead() || !srcDir.isDirectory()) {
             return;
@@ -86,6 +82,7 @@ public class Module {
             }
         }
     }
+
     private static void addIfNew(List source, List sink) {
         for (Iterator iter = source.iterator(); iter.hasNext();) {
             Object item = iter.next();
@@ -95,8 +92,9 @@ public class Module {
         }
     }
 
-    /** 
-     * Recursively find antecedant jars. 
+    /**
+     * Recursively find antecedant jars.
+     * 
      * @see findKnownJarAntecedants()
      */
     private static void doFindKnownJarAntecedants(Module module, List known) {
@@ -112,13 +110,13 @@ public class Module {
             }
         }
     }
-    
-    /**@return true if this is a source file */
+
+    /** @return true if this is a source file */
     private static boolean isSourceFile(File file) {
         String path = file.getPath();
-        return (path.endsWith(".java") || path.endsWith(".aj"));   // XXXFileLiteral
+        return (path.endsWith(".java") || path.endsWith(".aj")); // XXXFileLiteral
     }
-    
+
     public final boolean valid;
 
     public final File moduleDir;
@@ -127,24 +125,24 @@ public class Module {
 
     /** reference back to collection for creating required modules */
     final Modules modules;
-    
+
     /** path to output jar - may not exist */
     private final File moduleJar;
-    
+
     /** path to fully-assembed jar - may not exist */
     private final File assembledJar;
-    
+
     /** File list of library jars */
-    private final List libJars; 
-    
+    private final List libJars;
+
     /** String list of classpath variables */
     private final List classpathVariables;
 
-    /** 
-     * File list of library jars exported to clients 
-     * (duplicates some libJars entries) 
+    /**
+     * File list of library jars exported to clients (duplicates some libJars
+     * entries)
      */
-    private final List exportedLibJars; 
+    private final List exportedLibJars;
 
     /** File list of source directories */
     private final List srcDirs;
@@ -154,27 +152,23 @@ public class Module {
 
     /** Module list of required modules */
     private final List required;
-    
-    /** List of File that are newer than moduleJar.  Null until requested */
-    //private List newerFiles;
+
+    /** List of File that are newer than moduleJar. Null until requested */
+    // private List newerFiles;
     /** true if this has been found to be out of date */
     private boolean outOfDate;
-    
+
     /** true if we have calculated whether this is out of date */
     private boolean outOfDateSet;
-    
+
     /** if true, trim testing-related source directories, modules, and libraries */
     private final boolean trimTesting;
-    
+
     /** logger */
     private final Messager messager;
-    
-    Module(File moduleDir, 
-        File jarDir, 
-        String name, 
-        Modules modules, 
-        boolean trimTesting,
-        Messager messager) {
+
+    Module(File moduleDir, File jarDir, String name, Modules modules,
+            boolean trimTesting, Messager messager) {
         Util.iaxIfNotCanReadDir(moduleDir, "moduleDir");
         Util.iaxIfNotCanReadDir(jarDir, "jarDir");
         Util.iaxIfNull(name, "name");
@@ -190,51 +184,51 @@ public class Module {
         this.name = name;
         this.modules = modules;
         this.messager = messager;
-        this.moduleJar = new File(jarDir, name + ".jar");       
-        this.assembledJar = new File(jarDir, name + "-all.jar");       
+        this.moduleJar = new File(jarDir, name + ".jar");
+        this.assembledJar = new File(jarDir, name + "-all.jar");
         valid = init();
     }
-        
+
     /** @return path to output jar - may not exist */
     public File getModuleJar() {
         return moduleJar;
     }
-    
+
     /** @return path to output assembled jar - may not exist */
     public File getAssembledJar() {
         return assembledJar;
     }
-    
+
     /** @return unmodifiable List of String classpath variables */
     public List getClasspathVariables() {
         return Collections.unmodifiableList(classpathVariables);
     }
 
-    /** @return unmodifiable List of required modules String names*/
+    /** @return unmodifiable List of required modules String names */
     public List getRequired() {
         return Collections.unmodifiableList(required);
     }
-    
+
     /** @return unmodifiable list of exported library files, guaranteed readable */
     public List getExportedLibJars() {
         return Collections.unmodifiableList(exportedLibJars);
     }
-    
+
     /** @return unmodifiable list of required library files, guaranteed readable */
     public List getLibJars() {
         return Collections.unmodifiableList(libJars);
     }
-    
+
     /** @return unmodifiable list of source directories, guaranteed readable */
     public List getSrcDirs() {
         return Collections.unmodifiableList(srcDirs);
     }
-    
+
     /** @return Modules registry of known modules, including this one */
     public Modules getModules() {
         return modules;
     }
-    
+
     /** @return List of File jar paths to be merged into module-dist */
     public List getMerges() {
         String value = properties.getProperty(name + ".merges");
@@ -248,20 +242,18 @@ public class Module {
         }
         return result;
     }
-    
-   
+
     public void clearOutOfDate() {
         outOfDate = false;
         outOfDateSet = false;
     }
-    
+
     /**
-     * @param recalculate if true, then force recalculation 
-     * @return true if the target jar for this module is older than
-     *          any source files in a source directory
-     *          or any required modules
-     *          or any libraries
-     *          or if any libraries or required modules are missing
+     * @param recalculate
+     *            if true, then force recalculation
+     * @return true if the target jar for this module is older than any source
+     *         files in a source directory or any required modules or any
+     *         libraries or if any libraries or required modules are missing
      */
     public boolean outOfDate(boolean recalculate) {
         if (recalculate) {
@@ -276,8 +268,9 @@ public class Module {
                 final long time = moduleJar.lastModified();
                 File file;
                 for (Iterator iter = srcDirs.iterator(); iter.hasNext();) {
-                  File srcDir = (File) iter.next();
-                    for (Iterator srcFiles = sourceFiles(srcDir); srcFiles.hasNext();) {
+                    File srcDir = (File) iter.next();
+                    for (Iterator srcFiles = sourceFiles(srcDir); srcFiles
+                            .hasNext();) {
                         file = (File) srcFiles.next();
                         if (outOfDate(time, file)) {
                             return outOfDate = true;
@@ -305,33 +298,25 @@ public class Module {
         }
         return outOfDate;
     }
+
     /**
-     * Add any (File) library jar  or (File) required module jar
-     * to the List known, if not added already.
+     * Add any (File) library jar or (File) required module jar to the List
+     * known, if not added already.
      */
     public ArrayList findKnownJarAntecedants() {
         ArrayList result = new ArrayList();
         doFindKnownJarAntecedants(this, result);
-        return result;   
+        return result;
     }
-    
+
     public String toString() {
         return name;
     }
 
     public String toLongString() {
-        return  
-            "Module [name="
-            + name
-            + ", srcDirs="
-            + srcDirs
-            + ", required="
-            + required
-            + ", moduleJar="
-            + moduleJar
-            + ", libJars="
-            + libJars
-            + "]";
+        return "Module [name=" + name + ", srcDirs=" + srcDirs + ", required="
+                + required + ", moduleJar=" + moduleJar + ", libJars="
+                + libJars + "]";
     }
 
     private boolean init() {
@@ -341,19 +326,18 @@ public class Module {
     /** read eclipse .classpath file XXX line-oriented hack */
     private boolean initClasspath() {
         // meaning testsrc directory, junit library, etc.
-        File file = new File(moduleDir, ".classpath");   // XXXFileLiteral
+        File file = new File(moduleDir, ".classpath"); // XXXFileLiteral
         FileReader fin = null;
         try {
             fin = new FileReader(file);
             BufferedReader reader = new BufferedReader(fin);
             String line;
-            
-            XMLEntry entry = new XMLEntry("classpathentry", ATTS);
+            XMLItem item = new XMLItem("classpathentry", new ICB());
             while (null != (line = reader.readLine())) {
-                // we assume no internal spaces...
-                entry.acceptTokens(line);
-                if (entry.started && entry.ended) {
-                    update(entry);
+                line = line.trim();
+                // dumb - only handle comment-only lines
+                if (!line.startsWith("<?xml") && ! line.startsWith("<!--")) {
+                    item.acceptLine(line);
                 }
             }
             return (0 < (srcDirs.size() + libJars.size()));
@@ -361,16 +345,24 @@ public class Module {
             messager.logException("IOException reading " + file, e);
         } finally {
             if (null != fin) {
-                try { fin.close(); }
-                catch (IOException e) {} // ignore
+                try {
+                    fin.close();
+                } catch (IOException e) {
+                } // ignore
             }
         }
         return false;
     }
+
+    private boolean update(String toString, String[] attributes) {
+        String kind = attributes[getATTSIndex("kind")];
+        String path = attributes[getATTSIndex("path")];
+        String exp = attributes[getATTSIndex("exported")];
+        boolean exported = ("true".equals(exp));                 
+        return update(kind, path, toString, exported);
+    }
     
-    private boolean update(XMLEntry entry) {
-        String kind = entry.attributes[getATTSIndex("kind")];
-        String path = entry.attributes[getATTSIndex("path")];
+    private boolean update(String kind, String path, String toString, boolean exported) {    
         String libPath = null;
         if ("src".equals(kind)) {
             if (path.startsWith("/")) { // module
@@ -380,14 +372,14 @@ public class Module {
                     required.add(req);
                     return true;
                 } else {
-                    messager.error("update unable to create required module: " 
-                        + moduleName);
-                }                
-            } else {                    // src dir
+                    messager.error("update unable to create required module: "
+                            + moduleName);
+                }
+            } else { // src dir
                 String fullPath = getFullPath(path);
                 File srcDir = new File(fullPath);
                 if (srcDir.canRead() && srcDir.isDirectory()) {
-                    srcDirs.add(srcDir); 
+                    srcDirs.add(srcDir);
                     return true;
                 } else {
                     messager.error("not a src dir: " + srcDir);
@@ -410,49 +402,50 @@ public class Module {
                 }
             }
             if (null == libPath) {
-                warnVariable(path, entry);
-                classpathVariables.add(path);                
+                warnVariable(path, toString);
+                classpathVariables.add(path);
             }
         } else if ("con".equals(kind)) {
             if (-1 == path.indexOf("JRE")) { // warn non-JRE containers
-                messager.log("cannot handle con yet: " + entry);
+                messager.log("cannot handle con yet: " + toString);
             }
         } else if ("out".equals(kind) || "output".equals(kind)) {
             // ignore output entries
         } else {
-            messager.log("unrecognized kind " + kind + " in " + entry);
+            messager.log("unrecognized kind " + kind + " in " + toString);
         }
         if (null != libPath) {
-            File libJar= new File(libPath);
+            File libJar = new File(libPath);
             if (!libJar.exists()) {
                 libJar = new File(getFullPath(libPath));
             }
             if (libJar.canRead() && libJar.isFile()) {
-               libJars.add(libJar);
-               String exp = entry.attributes[getATTSIndex("exported")];
-               if ("true".equals(exp)) {
-                   exportedLibJars.add(libJar);
-               }
-               return true;
+                libJars.add(libJar);
+                if (exported) {
+                    exportedLibJars.add(libJar);
+                }
+                return true;
             } else {
-               messager.error("no such library jar " + libJar + " from " + entry);                
+                messager.error("no such library jar " + libJar + " from "
+                        + toString);
             }
         }
         return false;
     }
-    private void warnVariable(String path, XMLEntry entry) {
-        String[] known = {"JRE_LIB", "ASPECTJRT_LIB", "JRE15_LIB"};
+
+    private void warnVariable(String path, String toString) {
+        String[] known = { "JRE_LIB", "ASPECTJRT_LIB", "JRE15_LIB" };
         for (int i = 0; i < known.length; i++) {
             if (known[i].equals(path)) {
                 return;
             }
         }
-        messager.log("Module cannot handle var yet: " + entry);
+        messager.log("Module cannot handle var yet: " + toString);
     }
 
     /** @return true if any properties were read correctly */
     private boolean initProperties() {
-        File file = new File(moduleDir, name + ".properties");   // XXXFileLiteral
+        File file = new File(moduleDir, name + ".properties"); // XXXFileLiteral
         if (!Util.canReadFile(file)) {
             return true; // no properties to read
         }
@@ -466,35 +459,36 @@ public class Module {
             return false;
         } finally {
             if (null != fin) {
-                try { fin.close(); }
-                catch (IOException e) {} // ignore
+                try {
+                    fin.close();
+                } catch (IOException e) {
+                } // ignore
             }
         }
     }
-    
-    /** 
-     * Post-process initialization.  
-     * This implementation trims testing-related source 
-     * directories, libraries, and modules if trimTesting is enabled/true.  
-     * For modules whose names start with "testing",
-     * testing-related sources are trimmed, but this does not
-     * trim dependencies on other modules prefixed "testing"
-     * or on testing libraries like junit.  That means
-     * testing modules can be built with trimTesting enabled.
-     * @return true if initialization post-processing worked 
+
+    /**
+     * Post-process initialization. This implementation trims testing-related
+     * source directories, libraries, and modules if trimTesting is
+     * enabled/true. For modules whose names start with "testing",
+     * testing-related sources are trimmed, but this does not trim dependencies
+     * on other modules prefixed "testing" or on testing libraries like junit.
+     * That means testing modules can be built with trimTesting enabled.
+     * 
+     * @return true if initialization post-processing worked
      */
-    protected boolean reviewInit() {   
+    protected boolean reviewInit() {
         try {
             for (ListIterator iter = srcDirs.listIterator(); iter.hasNext();) {
                 File srcDir = (File) iter.next();
                 String lcname = srcDir.getName().toLowerCase();
-                if (trimTesting 
-                        && (Util.Constants.TESTSRC.equals(lcname)
-                           || Util.Constants.JAVA5_TESTSRC.equals(lcname))) { 
+                if (trimTesting
+                        && (Util.Constants.TESTSRC.equals(lcname) || Util.Constants.JAVA5_TESTSRC
+                                .equals(lcname))) {
                     iter.remove();
-                } else if (!Util.JAVA5_VM 
-                        && (Util.Constants.JAVA5_SRC.equals(lcname)
-                            || Util.Constants.JAVA5_TESTSRC.equals(lcname))) {
+                } else if (!Util.JAVA5_VM
+                        && (Util.Constants.JAVA5_SRC.equals(lcname) || Util.Constants.JAVA5_TESTSRC
+                                .equals(lcname))) {
                     // assume optional for pre-1.5 builds
                     iter.remove();
                 }
@@ -506,17 +500,18 @@ public class Module {
                 for (ListIterator iter = libJars.listIterator(); iter.hasNext();) {
                     File libJar = (File) iter.next();
                     String name = libJar.getName();
-                    if ("junit.jar".equals(name.toLowerCase())) {  // XXXFileLiteral              
+                    if ("junit.jar".equals(name.toLowerCase())) { // XXXFileLiteral
                         iter.remove(); // XXX if verbose log
-                    }   
+                    }
                 }
-                for (ListIterator iter = required.listIterator(); iter.hasNext();) {
+                for (ListIterator iter = required.listIterator(); iter
+                        .hasNext();) {
                     Module required = (Module) iter.next();
                     String name = required.name;
                     // XXX testing-util only ?
-                    if (name.toLowerCase().startsWith("testing")) {  // XXXFileLiteral
+                    if (name.toLowerCase().startsWith("testing")) { // XXXFileLiteral
                         iter.remove(); // XXX if verbose log
-                    }   
+                    }
                 }
             }
         } catch (UnsupportedOperationException e) {
@@ -524,19 +519,20 @@ public class Module {
         }
         return true;
     }
-    
+
     /** resolve path absolutely, assuming / means base of modules dir */
     public String getFullPath(String path) {
         String fullPath;
         if (path.startsWith("/")) {
             fullPath = modules.baseDir.getAbsolutePath() + path;
         } else {
-            fullPath = moduleDir.getAbsolutePath() + "/" + path; 
+            fullPath = moduleDir.getAbsolutePath() + "/" + path;
         }
         // check for absolute paths (untested - none in our modules so far)
         File testFile = new File(fullPath);
-        //System.out.println("Module.getFullPath: " + fullPath + " - " + testFile.getAbsolutePath());
-        if (! testFile.exists()) {
+        // System.out.println("Module.getFullPath: " + fullPath + " - " +
+        // testFile.getAbsolutePath());
+        if (!testFile.exists()) {
             testFile = new File(path);
             if (testFile.exists() && testFile.isAbsolute()) {
                 fullPath = path;
@@ -544,7 +540,7 @@ public class Module {
         }
         return fullPath;
     }
-    
+
     /** @return List of File of any module or library jar ending with suffix */
     private ArrayList findJarsBySuffix(String suffix) {
         ArrayList result = new ArrayList();
@@ -567,118 +563,189 @@ public class Module {
         }
         return result;
     }
-}
-/**
- * Extremely dumb class to parse XML entries
- * that contain no entities.
- */
-class XMLEntry {
-    static final String END = "/>";
-    static final String END_ATTRIBUTES = ">";
-    final String name;
-    final String startName;
-    final String endName;
-    final String[] attributeNames;
-    final String[] attributes;
-    final StringBuffer input;
-    boolean started;
-    boolean ended;
-    boolean attributesEnded;
-
-    XMLEntry(String name, String[] attributeNames) {
-        this.name = name;
-        this.attributeNames = attributeNames;
-        this.attributes = new String[attributeNames.length];
-        input = new StringBuffer();
-        startName = "<" + name;
-        endName = "</" + name;
-    }
-    
-    public void acceptTokens(String tokens) {
-        StringTokenizer st = new StringTokenizer(tokens);
-        while (st.hasMoreTokens()) {
-            acceptToken(st.nextToken());
-        }    
+
+    class ICB implements XMLItem.ICallback {
+        public void end(Properties attributes) {
+            String kind = attributes.getProperty("kind");
+            String path = attributes.getProperty("path");
+            String exp = attributes.getProperty("exported");
+            boolean exported = ("true".equals(exp));
+            ByteArrayOutputStream bout = new ByteArrayOutputStream();
+            attributes.list(new PrintStream(bout));
+            update(kind, path, bout.toString(), exported);
+        }
     }
-    
-    /**
-     * accept input (with no white space except that in values)
-     * Does not handle multi-token attributes, etc.
-     * @param s
-     */
-    public int acceptToken(String s) {
-        if ((null != s) || (0 < s.length())) {
-            input.append(s);
-            input.append(" ");
-            s = s.trim();
-            if (startName.equals(s)) {
-                reset();
-                started = true;
-            } else if (endName.equals(s) || END.equals(s)) {
-                ended = true;
-            } else if (END_ATTRIBUTES.equals(s)) {
-                if (started && !ended) {
-                    if (attributesEnded) {
-                        throw new IllegalStateException(s);
+
+    public static class XMLItem {
+        public interface ICallback {
+            void end(Properties attributes);
+        }
+        static final String START_NAME = "classpathentry";
+
+        static final String ATT_STARTED = "STARTED";
+
+        final ICallback callback;
+        final StringBuffer input = new StringBuffer();
+
+        final String[] attributes = new String[ATTS.length];
+        final String targetEntity;
+        String entityName;
+        String attributeName;
+
+        XMLItem(String targetEntity, ICallback callback) {
+            this.callback = callback;
+            this.targetEntity = targetEntity;
+            reset();
+            
+        }
+
+        private void reset() {
+            input.setLength(0);
+            for (int i = 0; i < attributes.length; i++) {
+                attributes[i] = null;
+            }
+            entityName = null;
+            attributeName = null;
+        }
+
+        String[] tokenize(String line) {
+            final String DELIM = " \n\t\\<>\"=";
+            StringTokenizer st = new StringTokenizer(line, DELIM, true);
+            ArrayList result = new ArrayList();
+            StringBuffer quote = new StringBuffer();
+            boolean inQuote = false;
+            while (st.hasMoreTokens()) {
+                String s = st.nextToken();
+                if ((1 == s.length()) && (-1 != DELIM.indexOf(s))) {
+                    if ("\"".equals(s)) { // end quote (or escaped)
+                        if (inQuote) {
+                            inQuote = false;
+                            quote.append("\"");
+                            result.add(quote.toString());
+                            quote.setLength(0);
+                        } else {
+                            quote.append("\"");
+                            inQuote = true;
+                        }
+                    } else {
+                        result.add(s);
+                    }
+                } else {  // not a delimiter
+                    if (inQuote) {
+                        quote.append(s);
                     } else {
-                        attributesEnded = true;
+                        result.add(s);
                     }
                 }
-            } else if (started && !attributesEnded) {
-                return readAttributes(s);
             }
+            return (String[]) result.toArray(new String[0]);
         }
-        return -1;
-    }
-    
-    public String toString() {
-        StringBuffer result = new StringBuffer();
-        result.append("<");
-        result.append(name);
-        for (int i = 0; i < attributeNames.length; i++) {
-            if (null != attributes[i]) {
-                result.append(" ");
-                result.append(attributeNames[i]);
-                result.append("=\"" + attributes[i] + "\"");            
+
+        public void acceptLine(String line) {
+            String[] tokens = tokenize(line);
+            for (int i = 0; i < tokens.length; i++) {
+                next(tokens[i]);
             }
         }
-        result.append("/>");
-        return result.toString();
-    }
-    
-    void reset() {
-        for (int i = 0; i < attributes.length; i++) {
-            attributes[i] = null;
+        
+        private Properties attributesToProperties() {
+            Properties result = new Properties();
+            for (int i = 0; i < attributes.length; i++) {
+                String a = attributes[i];
+                if (null != a) {
+                    result.setProperty(ATTS[i], a);
+                }
+            }
+            return result;
         }
-        started = false;
-        ended = false;
-        attributesEnded = false;
-        input.setLength(0);
-    }
 
-    /**
-     * 
-     * @param s one String attribute, optionally terminated with end
-     * @return
-     */
-    int readAttributes(String s) {
-        for (int i = 0; i < attributeNames.length; i++) {
-            if (s.startsWith(attributeNames[i] + "=\"")) {
-                int start = 2+attributeNames[i].length();
-                int endLoc = s.indexOf("\"", start);
-                if (-1 == endLoc) {
-                    throw new IllegalArgumentException(s);
+        void errorIfNotNull(String name, String value) {
+            if (null != value) {
+                error("Did not expect " + name + ": " + value);
+            }
+        }
+        
+        void errorIfNull(String name, String value) {
+            if (null == value) {
+                error("expected value for " + name);
+            }
+        }
+        
+        boolean activeEntity() {
+            return targetEntity.equals(entityName);
+        }
+        
+        /**
+         * Assumes that comments and "<?xml"-style lines are removed.
+         */
+        public void next(String s) {
+            if ((null == s) || (0 == s.length())) {
+                return;
+            }
+            input.append(s);
+            s = s.trim();
+            if (0 == s.length()) {
+                return;
+            }
+            if ("<".equals(s)) {
+                errorIfNotNull("entityName", entityName);
+                errorIfNotNull("attributeName", attributeName);
+            } else if (">".equals(s)) {
+                errorIfNull("entityName", entityName);
+                if ("/".equals(attributeName)) {
+                    attributeName = null;
+                } else {
+                    errorIfNotNull("attributeName", attributeName);
+                }
+                if (activeEntity()) {
+                    callback.end(attributesToProperties());
+                }
+                entityName = null;
+            } else if ("=".equals(s)) {
+                errorIfNull("entityName", entityName);
+                errorIfNull("attributeName", attributeName);
+            } else if (s.startsWith("\"")) {
+                errorIfNull("entityName", entityName);
+                errorIfNull("attributeName", attributeName);
+                writeAttribute(attributeName, s);
+                attributeName = null;
+            } else {
+                if (null == entityName) {
+                    reset();
+                    entityName = s;
+                } else if (null == attributeName) {
+                    attributeName = s;
+                } else {
+                    System.out.println("unknown state - not value, attribute, or entity: " + s);
+                }
+            }
+        }
+
+        void readAttribute(String s) {
+            for (int i = 0; i < ATTS.length; i++) {
+                if (s.equals(ATTS[i])) {
+                    attributes[i] = ATT_STARTED;
+                    break;
                 }
-                attributes[i] = s.substring(start, endLoc);
-                if (endLoc+1 < s.length()) {
-                    s = s.substring(endLoc+1);
-                    if (END.equals(s)) {
-                        ended = true;
+            }
+        }
+
+        void writeAttribute(String name, String value) {
+            for (int i = 0; i < ATTS.length; i++) {
+                if (name.equals(ATTS[i])) {
+                    if (!value.startsWith("\"") || !value.endsWith("\"")) {
+                        error("bad attribute value: " + value);
                     }
+                    value = value.substring(1, value.length() - 1);
+                    attributes[i] = value;
+                    return;
                 }
-                return i;
             }
         }
-        return -1;
+
+        void error(String s) {
+            throw new Error(s + " at input " + input);
+        }
     }
-} // class XMLEntry
+}
+