groovy-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From sun...@apache.org
Subject [24/47] groovy git commit: Move source files to proper packages
Date Wed, 20 Dec 2017 04:29:32 GMT
http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/GroovyScriptEngine.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/GroovyScriptEngine.java b/src/main/groovy/groovy/util/GroovyScriptEngine.java
new file mode 100644
index 0000000..92e2486
--- /dev/null
+++ b/src/main/groovy/groovy/util/GroovyScriptEngine.java
@@ -0,0 +1,694 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import groovy.lang.Binding;
+import groovy.lang.GroovyClassLoader;
+import groovy.lang.GroovyCodeSource;
+import groovy.lang.GroovyResourceLoader;
+import groovy.lang.Script;
+import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.InnerClassNode;
+import org.codehaus.groovy.classgen.GeneratorContext;
+import org.codehaus.groovy.control.ClassNodeResolver;
+import org.codehaus.groovy.control.CompilationFailedException;
+import org.codehaus.groovy.control.CompilationUnit;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.Phases;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.customizers.CompilationCustomizer;
+import org.codehaus.groovy.runtime.IOGroovyMethods;
+import org.codehaus.groovy.runtime.InvokerHelper;
+import org.codehaus.groovy.tools.gse.DependencyTracker;
+import org.codehaus.groovy.tools.gse.StringSetMap;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.ref.WeakReference;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.AccessController;
+import java.security.CodeSource;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Specific script engine able to reload modified scripts as well as dealing properly
+ * with dependent scripts.
+ *
+ * @author sam
+ * @author Marc Palmer
+ * @author Guillaume Laforge
+ * @author Jochen Theodorou
+ * @author Mattias Reichel
+ */
+public class GroovyScriptEngine implements ResourceConnector {
+    private static final ClassLoader CL_STUB = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
+        public ClassLoader run() {
+            return new ClassLoader() {};
+        }
+    });
+
+    private static final URL[] EMPTY_URL_ARRAY = new URL[0];
+
+    private static class LocalData {
+        CompilationUnit cu;
+        final StringSetMap dependencyCache = new StringSetMap();
+        final Map<String, String> precompiledEntries = new HashMap<String, String>();
+    }
+
+    private static WeakReference<ThreadLocal<LocalData>> localData = new WeakReference<ThreadLocal<LocalData>>(null);
+
+    private static synchronized ThreadLocal<LocalData> getLocalData() {
+        ThreadLocal<LocalData> local = localData.get();
+        if (local != null) return local;
+        local = new ThreadLocal<LocalData>();
+        localData = new WeakReference<ThreadLocal<LocalData>>(local);
+        return local;
+    }
+
+    private final URL[] roots;
+    private final ResourceConnector rc;
+    private final ClassLoader parentLoader;
+    private GroovyClassLoader groovyLoader;
+    private final Map<String, ScriptCacheEntry> scriptCache = new ConcurrentHashMap<String, ScriptCacheEntry>();
+    private CompilerConfiguration config;
+
+    {
+        config = new CompilerConfiguration(CompilerConfiguration.DEFAULT);
+        config.setSourceEncoding(CompilerConfiguration.DEFAULT_SOURCE_ENCODING);
+    }
+
+
+    //TODO: more finals?
+
+    private static class ScriptCacheEntry {
+        private final Class scriptClass;
+        private final long lastModified, lastCheck;
+        private final Set<String> dependencies;
+        private final boolean sourceNewer;
+
+        public ScriptCacheEntry(Class clazz, long modified, long lastCheck, Set<String> depend, boolean sourceNewer) {
+            this.scriptClass = clazz;
+            this.lastModified = modified;
+            this.lastCheck = lastCheck;
+            this.dependencies = depend;
+            this.sourceNewer = sourceNewer;
+        }
+
+        public ScriptCacheEntry(ScriptCacheEntry old, long lastCheck, boolean sourceNewer) {
+            this(old.scriptClass, old.lastModified, lastCheck, old.dependencies, sourceNewer);
+        }
+    }
+
+    private class ScriptClassLoader extends GroovyClassLoader {
+
+
+        public ScriptClassLoader(GroovyClassLoader loader) {
+            super(loader);
+        }
+
+        public ScriptClassLoader(ClassLoader loader, CompilerConfiguration config) {
+            super(loader, config, false);
+            setResLoader();
+        }
+
+        private void setResLoader() {
+            final GroovyResourceLoader rl = getResourceLoader();
+            setResourceLoader(new GroovyResourceLoader() {
+                public URL loadGroovySource(String className) throws MalformedURLException {
+                    String filename;
+                    for (String extension : getConfig().getScriptExtensions()) {
+                        filename = className.replace('.', File.separatorChar) + "." + extension;
+                        try {
+                            URLConnection dependentScriptConn = rc.getResourceConnection(filename);
+                            return dependentScriptConn.getURL();
+                        } catch (ResourceException e) {
+                            //TODO: maybe do something here?
+                        }
+                    }
+                    return rl.loadGroovySource(className);
+                }
+            });
+        }
+
+        @Override
+        protected CompilationUnit createCompilationUnit(CompilerConfiguration configuration, CodeSource source) {
+            CompilationUnit cu = super.createCompilationUnit(configuration, source);
+            LocalData local = getLocalData().get();
+            local.cu = cu;
+            final StringSetMap cache = local.dependencyCache;
+            final Map<String, String> precompiledEntries = local.precompiledEntries;
+
+            // "." is used to transfer compilation dependencies, which will be
+            // recollected later during compilation
+            for (String depSourcePath : cache.get(".")) {
+                try {
+                    cache.get(depSourcePath);
+                    cu.addSource(getResourceConnection(depSourcePath).getURL());
+                } catch (ResourceException e) {
+                    /* ignore */
+                }
+            }
+
+            // remove all old entries including the "." entry
+            cache.clear();
+
+            cu.addPhaseOperation(new CompilationUnit.PrimaryClassNodeOperation() {
+                @Override
+                public void call(final SourceUnit source, GeneratorContext context, ClassNode classNode)
+                        throws CompilationFailedException {
+                    // GROOVY-4013: If it is an inner class, tracking its dependencies doesn't really
+                    // serve any purpose and also interferes with the caching done to track dependencies
+                    if (classNode instanceof InnerClassNode) return;
+                    DependencyTracker dt = new DependencyTracker(source, cache, precompiledEntries);
+                    dt.visitClass(classNode);
+                }
+            }, Phases.CLASS_GENERATION);
+
+            cu.setClassNodeResolver(new ClassNodeResolver() {
+                @Override
+                public LookupResult findClassNode(String origName, CompilationUnit compilationUnit) {
+                    CompilerConfiguration cc = compilationUnit.getConfiguration();
+                    String name = origName.replace('.', '/');
+                    for (String ext : cc.getScriptExtensions()) {
+                        try {
+                            String finalName = name + "." + ext;
+                            URLConnection conn = rc.getResourceConnection(finalName);
+                            URL url = conn.getURL();
+                            String path = url.toExternalForm();
+                            ScriptCacheEntry entry = scriptCache.get(path);
+                            Class clazz = null;
+                            if (entry != null) clazz = entry.scriptClass;
+                            if (GroovyScriptEngine.this.isSourceNewer(entry)) {
+                                try {
+                                    SourceUnit su = compilationUnit.addSource(url);
+                                    return new LookupResult(su, null);
+                                } finally {
+                                    forceClose(conn);
+                                }
+                            } else {
+                                precompiledEntries.put(origName, path);
+                            }
+                            if (clazz != null) {
+                                ClassNode cn = new ClassNode(clazz);
+                                return new LookupResult(null, cn);
+                            }
+                        } catch (ResourceException re) {
+                            // skip
+                        }
+                    }
+                    return super.findClassNode(origName, compilationUnit);
+                }
+            });
+
+            return cu;
+        }
+
+        @Override
+        public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException {
+            synchronized (sourceCache) {
+                return doParseClass(codeSource);
+            }
+        }
+
+        private Class<?> doParseClass(GroovyCodeSource codeSource) {
+            // local is kept as hard reference to avoid garbage collection
+            ThreadLocal<LocalData> localTh = getLocalData();
+            LocalData localData = new LocalData();
+            localTh.set(localData);
+            StringSetMap cache = localData.dependencyCache;
+            Class<?> answer = null;
+            try {
+                updateLocalDependencyCache(codeSource, localData);
+                answer = super.parseClass(codeSource, false);
+                updateScriptCache(localData);
+            } finally {
+                cache.clear();
+                localTh.remove();
+            }
+            return answer;
+        }
+
+        private void updateLocalDependencyCache(GroovyCodeSource codeSource, LocalData localData) {
+            // we put the old dependencies into local cache so createCompilationUnit
+            // can pick it up. We put that entry under the name "."
+            ScriptCacheEntry origEntry = scriptCache.get(codeSource.getName());
+            Set<String> origDep = null;
+            if (origEntry != null) origDep = origEntry.dependencies;
+            if (origDep != null) {
+                Set<String> newDep = new HashSet<String>(origDep.size());
+                for (String depName : origDep) {
+                    ScriptCacheEntry dep = scriptCache.get(depName);
+                    try {
+                        if (origEntry == dep || GroovyScriptEngine.this.isSourceNewer(dep)) {
+                            newDep.add(depName);
+                        }
+                    } catch (ResourceException re) {
+
+                    }
+                }
+                StringSetMap cache = localData.dependencyCache;
+                cache.put(".", newDep);
+            }
+        }
+
+        private void updateScriptCache(LocalData localData) {
+            StringSetMap cache = localData.dependencyCache;
+            cache.makeTransitiveHull();
+            long time = getCurrentTime();
+            Set<String> entryNames = new HashSet<String>();
+            for (Map.Entry<String, Set<String>> entry : cache.entrySet()) {
+                String className = entry.getKey();
+                Class clazz = getClassCacheEntry(className);
+                if (clazz == null) continue;
+
+                String entryName = getPath(clazz, localData.precompiledEntries);
+                if (entryNames.contains(entryName)) continue;
+                entryNames.add(entryName);
+                Set<String> value = convertToPaths(entry.getValue(), localData.precompiledEntries);
+                long lastModified;
+                try {
+                    lastModified = getLastModified(entryName);
+                } catch (ResourceException e) {
+                    lastModified = time;
+                }
+                ScriptCacheEntry cacheEntry = new ScriptCacheEntry(clazz, lastModified, time, value, false);
+                scriptCache.put(entryName, cacheEntry);
+            }
+        }
+
+        private String getPath(Class clazz, Map<String, String> precompiledEntries) {
+            CompilationUnit cu = getLocalData().get().cu;
+            String name = clazz.getName();
+            ClassNode classNode = cu.getClassNode(name);
+            if (classNode == null) {
+                // this is a precompiled class!
+                String path = precompiledEntries.get(name);
+                if (path == null) throw new GroovyBugError("Precompiled class " + name + " should be available in precompiled entries map, but was not.");
+                return path;
+            } else {
+                return classNode.getModule().getContext().getName();
+            }
+        }
+
+        private Set<String> convertToPaths(Set<String> orig, Map<String, String> precompiledEntries) {
+            Set<String> ret = new HashSet<String>();
+            for (String className : orig) {
+                Class clazz = getClassCacheEntry(className);
+                if (clazz == null) continue;
+                ret.add(getPath(clazz, precompiledEntries));
+            }
+            return ret;
+        }
+    }
+
+    /**
+     * Simple testing harness for the GSE. Enter script roots as arguments and
+     * then input script names to run them.
+     *
+     * @param urls an array of URLs
+     * @throws Exception if something goes wrong
+     */
+    public static void main(String[] urls) throws Exception {
+        GroovyScriptEngine gse = new GroovyScriptEngine(urls);
+        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+        String line;
+        while (true) {
+            System.out.print("groovy> ");
+            if ((line = br.readLine()) == null || line.equals("quit")) {
+                break;
+            }
+            try {
+                System.out.println(gse.run(line, new Binding()));
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Initialize a new GroovyClassLoader with a default or
+     * constructor-supplied parentClassLoader.
+     *
+     * @return the parent classloader used to load scripts
+     */
+    private GroovyClassLoader initGroovyLoader() {
+        GroovyClassLoader groovyClassLoader =
+                AccessController.doPrivileged(new PrivilegedAction<ScriptClassLoader>() {
+                    public ScriptClassLoader run() {
+                        if (parentLoader instanceof GroovyClassLoader) {
+                            return new ScriptClassLoader((GroovyClassLoader) parentLoader);
+                        } else {
+                            return new ScriptClassLoader(parentLoader, config);
+                        }
+                    }
+                });
+        for (URL root : roots) groovyClassLoader.addURL(root);
+        return groovyClassLoader;
+    }
+
+    /**
+     * Get a resource connection as a <code>URLConnection</code> to retrieve a script
+     * from the <code>ResourceConnector</code>.
+     *
+     * @param resourceName name of the resource to be retrieved
+     * @return a URLConnection to the resource
+     * @throws ResourceException
+     */
+    public URLConnection getResourceConnection(String resourceName) throws ResourceException {
+        // Get the URLConnection
+        URLConnection groovyScriptConn = null;
+
+        ResourceException se = null;
+        for (URL root : roots) {
+            URL scriptURL = null;
+            try {
+                scriptURL = new URL(root, resourceName);
+                groovyScriptConn = openConnection(scriptURL);
+
+                break; // Now this is a bit unusual
+            } catch (MalformedURLException e) {
+                String message = "Malformed URL: " + root + ", " + resourceName;
+                if (se == null) {
+                    se = new ResourceException(message);
+                } else {
+                    se = new ResourceException(message, se);
+                }
+            } catch (IOException e1) {
+                String message = "Cannot open URL: " + root + resourceName;
+                groovyScriptConn = null;
+                if (se == null) {
+                    se = new ResourceException(message);
+                } else {
+                    se = new ResourceException(message, se);
+                }
+            }
+        }
+
+        if (se == null) se = new ResourceException("No resource for " + resourceName + " was found");
+
+        // If we didn't find anything, report on all the exceptions that occurred.
+        if (groovyScriptConn == null) throw se;
+        return groovyScriptConn;
+    }
+
+    private static URLConnection openConnection(URL scriptURL) throws IOException {
+        URLConnection urlConnection = scriptURL.openConnection();
+        verifyInputStream(urlConnection);
+
+        return scriptURL.openConnection();
+    }
+
+    /**
+     * This method closes a {@link URLConnection} by getting its {@link InputStream} and calling the
+     * {@link InputStream#close()} method on it. The {@link URLConnection} doesn't have a close() method
+     * and relies on garbage collection to close the underlying connection to the file.
+     * Relying on garbage collection could lead to the application exhausting the number of files the
+     * user is allowed to have open at any one point in time and cause the application to crash
+     * ({@link java.io.FileNotFoundException} (Too many open files)).
+     * Hence the need for this method to explicitly close the underlying connection to the file.
+     *
+     * @param urlConnection the {@link URLConnection} to be "closed" to close the underlying file descriptors.
+     */
+    private static void forceClose(URLConnection urlConnection) {
+        if (urlConnection != null) {
+            // We need to get the input stream and close it to force the open
+            // file descriptor to be released. Otherwise, we will reach the limit
+            // for number of files open at one time.
+
+            try {
+                verifyInputStream(urlConnection);
+            } catch (Exception e) {
+                // Do nothing: We were not going to use it anyway.
+            }
+        }
+    }
+
+    private static void verifyInputStream(URLConnection urlConnection) throws IOException {
+        InputStream in = null;
+        try {
+            in = urlConnection.getInputStream();
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException ignore) {
+                }
+            }
+        }
+    }
+
+    /**
+     * The groovy script engine will run groovy scripts and reload them and
+     * their dependencies when they are modified. This is useful for embedding
+     * groovy in other containers like games and application servers.
+     *
+     * @param roots This an array of URLs where Groovy scripts will be stored. They should
+     *              be laid out using their package structure like Java classes
+     */
+    private GroovyScriptEngine(URL[] roots, ClassLoader parent, ResourceConnector rc) {
+        if (roots == null) roots = EMPTY_URL_ARRAY;
+        this.roots = roots;
+        if (rc == null) rc = this;
+        this.rc = rc;
+        if (parent == CL_STUB) parent = this.getClass().getClassLoader();
+        this.parentLoader = parent;
+        this.groovyLoader = initGroovyLoader();
+    }
+
+    public GroovyScriptEngine(URL[] roots) {
+        this(roots, CL_STUB, null);
+    }
+
+    public GroovyScriptEngine(URL[] roots, ClassLoader parentClassLoader) {
+        this(roots, parentClassLoader, null);
+    }
+
+    public GroovyScriptEngine(String[] urls) throws IOException {
+        this(createRoots(urls), CL_STUB, null);
+    }
+
+    private static URL[] createRoots(String[] urls) throws MalformedURLException {
+        if (urls == null) return null;
+        URL[] roots = new URL[urls.length];
+        for (int i = 0; i < roots.length; i++) {
+            if (urls[i].contains("://")) {
+                roots[i] = new URL(urls[i]);
+            } else {
+                roots[i] = new File(urls[i]).toURI().toURL();
+            }
+        }
+        return roots;
+    }
+
+    public GroovyScriptEngine(String[] urls, ClassLoader parentClassLoader) throws IOException {
+        this(createRoots(urls), parentClassLoader, null);
+    }
+
+    public GroovyScriptEngine(String url) throws IOException {
+        this(new String[]{url});
+    }
+
+    public GroovyScriptEngine(String url, ClassLoader parentClassLoader) throws IOException {
+        this(new String[]{url}, parentClassLoader);
+    }
+
+    public GroovyScriptEngine(ResourceConnector rc) {
+        this(null, CL_STUB, rc);
+    }
+
+    public GroovyScriptEngine(ResourceConnector rc, ClassLoader parentClassLoader) {
+        this(null, parentClassLoader, rc);
+    }
+
+    /**
+     * Get the <code>ClassLoader</code> that will serve as the parent ClassLoader of the
+     * {@link GroovyClassLoader} in which scripts will be executed. By default, this is the
+     * ClassLoader that loaded the <code>GroovyScriptEngine</code> class.
+     *
+     * @return the parent classloader used to load scripts
+     */
+    public ClassLoader getParentClassLoader() {
+        return parentLoader;
+    }
+
+    /**
+     * Get the class of the scriptName in question, so that you can instantiate
+     * Groovy objects with caching and reloading.
+     *
+     * @param scriptName resource name pointing to the script
+     * @return the loaded scriptName as a compiled class
+     * @throws ResourceException if there is a problem accessing the script
+     * @throws ScriptException   if there is a problem parsing the script
+     */
+    public Class loadScriptByName(String scriptName) throws ResourceException, ScriptException {
+        URLConnection conn = rc.getResourceConnection(scriptName);
+        String path = conn.getURL().toExternalForm();
+        ScriptCacheEntry entry = scriptCache.get(path);
+        Class clazz = null;
+        if (entry != null) clazz = entry.scriptClass;
+        try {
+            if (isSourceNewer(entry)) {
+                try {
+                    String encoding = conn.getContentEncoding() != null ? conn.getContentEncoding() : config.getSourceEncoding();
+                    String content = IOGroovyMethods.getText(conn.getInputStream(), encoding);
+                    clazz = groovyLoader.parseClass(content, path);
+                } catch (IOException e) {
+                    throw new ResourceException(e);
+                }
+            }
+        } finally {
+            forceClose(conn);
+        }
+        return clazz;
+    }
+
+    /**
+     * Run a script identified by name with a single argument.
+     *
+     * @param scriptName name of the script to run
+     * @param argument   a single argument passed as a variable named <code>arg</code> in the binding
+     * @return a <code>toString()</code> representation of the result of the execution of the script
+     * @throws ResourceException if there is a problem accessing the script
+     * @throws ScriptException   if there is a problem parsing the script
+     */
+    public String run(String scriptName, String argument) throws ResourceException, ScriptException {
+        Binding binding = new Binding();
+        binding.setVariable("arg", argument);
+        Object result = run(scriptName, binding);
+        return result == null ? "" : result.toString();
+    }
+
+    /**
+     * Run a script identified by name with a given binding.
+     *
+     * @param scriptName name of the script to run
+     * @param binding    the binding to pass to the script
+     * @return an object
+     * @throws ResourceException if there is a problem accessing the script
+     * @throws ScriptException   if there is a problem parsing the script
+     */
+    public Object run(String scriptName, Binding binding) throws ResourceException, ScriptException {
+        return createScript(scriptName, binding).run();
+    }
+
+    /**
+     * Creates a Script with a given scriptName and binding.
+     *
+     * @param scriptName name of the script to run
+     * @param binding    the binding to pass to the script
+     * @return the script object
+     * @throws ResourceException if there is a problem accessing the script
+     * @throws ScriptException   if there is a problem parsing the script
+     */
+    public Script createScript(String scriptName, Binding binding) throws ResourceException, ScriptException {
+        return InvokerHelper.createScript(loadScriptByName(scriptName), binding);
+    }
+
+    private long getLastModified(String scriptName) throws ResourceException {
+        URLConnection conn = rc.getResourceConnection(scriptName);
+        long lastMod = 0;
+        try {
+            lastMod = conn.getLastModified();
+        } finally {
+            // getResourceConnection() opening the inputstream, let's ensure all streams are closed
+            forceClose(conn);
+        }
+        return lastMod;
+    }
+
+    protected boolean isSourceNewer(ScriptCacheEntry entry) throws ResourceException {
+        if (entry == null) return true;
+
+        long mainEntryLastCheck = entry.lastCheck;
+        long now = 0;
+
+        boolean returnValue = false;
+        for (String scriptName : entry.dependencies) {
+            ScriptCacheEntry depEntry = scriptCache.get(scriptName);
+            if (depEntry.sourceNewer) return true;
+
+            // check if maybe dependency was recompiled, but this one here not
+            if (mainEntryLastCheck < depEntry.lastModified) {
+                returnValue = true;
+                continue;
+            }
+
+            if (now == 0) now = getCurrentTime();
+            long nextSourceCheck = depEntry.lastCheck + config.getMinimumRecompilationInterval();
+            if (nextSourceCheck > now) continue;
+
+            long lastMod = getLastModified(scriptName);
+            if (depEntry.lastModified < lastMod) {
+                depEntry = new ScriptCacheEntry(depEntry, lastMod, true);
+                scriptCache.put(scriptName, depEntry);
+                returnValue = true;
+            } else {
+                depEntry = new ScriptCacheEntry(depEntry, now, false);
+                scriptCache.put(scriptName, depEntry);
+            }
+        }
+
+        return returnValue;
+    }
+
+    /**
+     * Returns the GroovyClassLoader associated with this script engine instance.
+     * Useful if you need to pass the class loader to another library.
+     *
+     * @return the GroovyClassLoader
+     */
+    public GroovyClassLoader getGroovyClassLoader() {
+        return groovyLoader;
+    }
+
+    /**
+     * @return a non null compiler configuration
+     */
+    public CompilerConfiguration getConfig() {
+        return config;
+    }
+
+    /**
+     * sets a compiler configuration
+     *
+     * @param config - the compiler configuration
+     * @throws NullPointerException if config is null
+     */
+    public void setConfig(CompilerConfiguration config) {
+        if (config == null) throw new NullPointerException("configuration cannot be null");
+        this.config = config;
+        this.groovyLoader = initGroovyLoader();
+    }
+
+    protected long getCurrentTime() {
+        return System.currentTimeMillis();
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/IFileNameFinder.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/IFileNameFinder.java b/src/main/groovy/groovy/util/IFileNameFinder.java
new file mode 100644
index 0000000..35e9012
--- /dev/null
+++ b/src/main/groovy/groovy/util/IFileNameFinder.java
@@ -0,0 +1,26 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import java.util.List;
+
+public interface IFileNameFinder {
+    List<String> getFileNames(String basedir, String pattern);
+    List<String> getFileNames(String basedir, String pattern, String excludesPattern);
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/IndentPrinter.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/IndentPrinter.java b/src/main/groovy/groovy/util/IndentPrinter.java
new file mode 100644
index 0000000..df3d8c2
--- /dev/null
+++ b/src/main/groovy/groovy/util/IndentPrinter.java
@@ -0,0 +1,234 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import groovy.lang.GroovyRuntimeException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+
+/**
+ * A helper class for printing indented text. This can be used stand-alone or, more commonly, from Builders.
+ * <p>
+ * By default, a PrintWriter to System.out is used as the Writer, but it is possible
+ * to change the Writer by passing a new one as a constructor argument.
+ * <p>
+ * Indention by default is 2 characters but can be changed by passing a
+ * different value as a constructor argument.
+ * <p>
+ * The following is an example usage. Note that within a "with" block you need to
+ * specify a parameter name so that this.println is not called instead of IndentPrinter.println:
+ * <pre>
+ * new IndentPrinter(new PrintWriter(out)).with { p ->
+ *     p.printIndent()
+ *     p.println('parent1')
+ *     p.incrementIndent()
+ *     p.printIndent()
+ *     p.println('child 1')
+ *     p.printIndent()
+ *     p.println('child 2')
+ *     p.decrementIndent()
+ *     p.printIndent()
+ *     p.println('parent2')
+ *     p.flush()
+ * }
+ * </pre>
+ * The above example prints this to standard output:
+ * <pre>
+ * parent1
+ *   child 1
+ *   child 2
+ * parent2
+ * </pre>
+ *
+ * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
+ */
+public class IndentPrinter {
+
+    private int indentLevel;
+    private final String indent;
+    private final Writer out;
+    private final boolean addNewlines;
+    private boolean autoIndent;
+
+    /**
+     * Creates an IndentPrinter backed by a PrintWriter pointing to System.out, with an indent of two spaces.
+     *
+     * @see #IndentPrinter(Writer, String)
+     */
+    public IndentPrinter() {
+        this(new PrintWriter(System.out), "  ");
+    }
+
+    /**
+     * Creates an IndentPrinter backed by the supplied Writer, with an indent of two spaces.
+     *
+     * @param out Writer to output to
+     * @see #IndentPrinter(Writer, String)
+     */
+    public IndentPrinter(Writer out) {
+        this(out, "  ");
+    }
+
+    /**
+     * Creates an IndentPrinter backed by the supplied Writer,
+     * with a user-supplied String to be used for indenting.
+     *
+     * @param out Writer to output to
+     * @param indent character(s) used to indent each line
+     */
+    public IndentPrinter(Writer out, String indent) {
+        this(out, indent, true);
+    }
+
+    /**
+     * Creates an IndentPrinter backed by the supplied Writer,
+     * with a user-supplied String to be used for indenting
+     * and the ability to override newline handling.
+     *
+     * @param out Writer to output to
+     * @param indent character(s) used to indent each line
+     * @param addNewlines set to false to gobble all new lines (default true)
+     */
+    public IndentPrinter(Writer out, String indent, boolean addNewlines) {
+       this(out, indent, addNewlines, false);
+    }
+
+    /**
+     * Create an IndentPrinter to the given PrintWriter
+     * @param out Writer to output to
+     * @param indent character(s) used to indent each line
+     * @param addNewlines set to false to gobble all new lines (default true)
+     * @param autoIndent set to true to make println() prepend the indent automatically (default false)
+     */
+    public IndentPrinter(Writer out, String indent, boolean addNewlines, boolean autoIndent) {
+        this.addNewlines = addNewlines;
+        if (out == null) {
+            throw new IllegalArgumentException("Must specify a Writer");
+        }
+        this.out = out;
+        this.indent = indent;
+        this.autoIndent = autoIndent;
+    }
+
+    /**
+     * Prints a string followed by an end of line character.
+     *
+     * @param  text String to be written
+     */
+    public void println(String text) {
+        try {
+            if(autoIndent) printIndent();
+            out.write(text);
+            println();
+        } catch(IOException ioe) {
+            throw new GroovyRuntimeException(ioe);
+        }
+    }
+
+    /**
+     * Prints a string.
+     *
+     * @param  text String to be written
+     */
+    public void print(String text) {
+        try {
+            out.write(text);
+        } catch(IOException ioe) {
+            throw new GroovyRuntimeException(ioe);
+        }
+    }
+
+    /**
+     * Prints a character.
+     *
+     * @param  c char to be written
+     */
+    public void print(char c) {
+        try {
+            out.write(c);
+        } catch(IOException ioe) {
+            throw new GroovyRuntimeException(ioe);
+        }
+    }
+
+    /**
+     * Prints the current indent level.
+     */
+    public void printIndent() {
+        for (int i = 0; i < indentLevel; i++) {
+            try {
+                out.write(indent);
+            } catch(IOException ioe) {
+                throw new GroovyRuntimeException(ioe);
+            }
+        }
+    }
+
+    /**
+     * Prints an end-of-line character (if enabled via addNewLines property).
+     * Defaults to outputting a single '\n' character but by using a custom
+     * Writer, e.g. PlatformLineWriter, you can get platform-specific
+     * end-of-line characters.
+     *
+     * @see #IndentPrinter(Writer, String, boolean)
+     */
+    public void println() {
+        if (addNewlines) {
+            try {
+                out.write("\n");
+            } catch(IOException ioe) {
+                throw new GroovyRuntimeException(ioe);
+            }
+        }
+    }
+
+    public void incrementIndent() {
+        ++indentLevel;
+    }
+
+    public void decrementIndent() {
+        --indentLevel;
+    }
+
+    public int getIndentLevel() {
+        return indentLevel;
+    }
+
+    public void setIndentLevel(int indentLevel) {
+        this.indentLevel = indentLevel;
+    }
+
+    public boolean getAutoIndent(){
+        return this.autoIndent;
+    }
+
+    public void setAutoIndent(boolean autoIndent){
+        this.autoIndent = autoIndent;
+    }
+
+    public void flush() {
+        try {
+            out.flush();
+        } catch(IOException ioe) {
+            throw new GroovyRuntimeException(ioe);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/MapEntry.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/MapEntry.java b/src/main/groovy/groovy/util/MapEntry.java
new file mode 100644
index 0000000..9190fcd
--- /dev/null
+++ b/src/main/groovy/groovy/util/MapEntry.java
@@ -0,0 +1,83 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
+
+import java.util.Map;
+
+/**
+ * A Map.Entry implementation.
+ * 
+ * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
+ */
+public class MapEntry implements Map.Entry {
+
+    private Object key;
+    private Object value;
+
+    public MapEntry(Object key, Object value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    public boolean equals(Object that) {
+        if (that instanceof MapEntry) {
+            return equals((MapEntry) that);
+        }
+        return false;
+    }
+
+    public boolean equals(MapEntry that) {
+        return DefaultTypeTransformation.compareEqual(this.key, that.key) && DefaultTypeTransformation.compareEqual(this.value, that.value);
+    }
+
+    public int hashCode() {
+        return hash(key) ^ hash(value);
+    }
+
+    public String toString() {
+        return "" + key + ":" + value;
+    }
+
+    public Object getKey() {
+        return key;
+    }
+
+    public void setKey(Object key) {
+        this.key = key;
+    }
+
+    public Object getValue() {
+        return value;
+    }
+
+    public Object setValue(Object value) {
+        this.value = value;
+        return value;
+    }
+
+    /**
+     * Helper method to handle object hashes for possibly null values
+     */
+    protected int hash(Object object) {
+        return (object == null) ? 0xbabe : object.hashCode();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/Node.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/Node.java b/src/main/groovy/groovy/util/Node.java
new file mode 100644
index 0000000..e40b14a
--- /dev/null
+++ b/src/main/groovy/groovy/util/Node.java
@@ -0,0 +1,787 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import groovy.lang.Closure;
+import groovy.lang.DelegatingMetaClass;
+import groovy.lang.GroovySystem;
+import groovy.lang.MetaClass;
+import groovy.lang.Tuple2;
+import groovy.xml.QName;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+import org.codehaus.groovy.runtime.InvokerHelper;
+import org.codehaus.groovy.util.ListHashMap;
+
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+/**
+ * Represents an arbitrary tree node which can be used for structured metadata or any arbitrary XML-like tree.
+ * A node can have a name, a value and an optional Map of attributes.
+ * Typically the name is a String and a value is either a String or a List of other Nodes,
+ * though the types are extensible to provide a flexible structure, e.g. you could use a
+ * QName as the name which includes a namespace URI and a local name. Or a JMX ObjectName etc.
+ * So this class can represent metadata like <code>{foo a=1 b="abc"}</code> or nested
+ * metadata like <code>{foo a=1 b="123" { bar x=12 text="hello" }}</code>
+ *
+ * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
+ * @author Paul King
+ */
+public class Node implements Serializable, Cloneable {
+
+    static {
+        // wrap the standard MetaClass with the delegate
+        setMetaClass(GroovySystem.getMetaClassRegistry().getMetaClass(Node.class), Node.class);
+    }
+
+    private static final long serialVersionUID = 4121134753270542643L;
+
+    private Node parent;
+
+    private final Object name;
+
+    private final Map attributes;
+
+    private Object value;
+
+    /**
+     * Creates a new Node with the same name, no parent, shallow cloned attributes
+     * and if the value is a NodeList, a (deep) clone of those nodes.
+     *
+     * @return the clone
+     */
+    @Override
+    public Object clone() {
+        Object newValue = value;
+        if (value != null && value instanceof NodeList) {
+            NodeList nodes = (NodeList) value;
+            newValue = nodes.clone();
+        }
+        return new Node(null, name, new HashMap(attributes), newValue);
+    }
+
+    /**
+     * Creates a new Node named <code>name</code> and if a parent is supplied, adds
+     * the newly created node as a child of the parent.
+     *
+     * @param parent the parent node or null if no parent
+     * @param name   the name of the node
+     */
+    public Node(Node parent, Object name) {
+        this(parent, name, new NodeList());
+    }
+
+    /**
+     * Creates a new Node named <code>name</code> with value <code>value</code> and
+     * if a parent is supplied, adds the newly created node as a child of the parent.
+     *
+     * @param parent the parent node or null if no parent
+     * @param name   the name of the node
+     * @param value  the Node value, e.g. some text but in general any Object
+     */
+    public Node(Node parent, Object name, Object value) {
+        this(parent, name, new HashMap(), value);
+    }
+
+    /**
+     * Creates a new Node named <code>name</code> with
+     * attributes specified in the <code>attributes</code> Map. If a parent is supplied,
+     * the newly created node is added as a child of the parent.
+     *
+     * @param parent     the parent node or null if no parent
+     * @param name       the name of the node
+     * @param attributes a Map of name-value pairs
+     */
+    public Node(Node parent, Object name, Map attributes) {
+        this(parent, name, attributes, new NodeList());
+    }
+
+    /**
+     * Creates a new Node named <code>name</code> with value <code>value</code> and
+     * with attributes specified in the <code>attributes</code> Map. If a parent is supplied,
+     * the newly created node is added as a child of the parent.
+     *
+     * @param parent     the parent node or null if no parent
+     * @param name       the name of the node
+     * @param attributes a Map of name-value pairs
+     * @param value      the Node value, e.g. some text but in general any Object
+     */
+    public Node(Node parent, Object name, Map attributes, Object value) {
+        this.parent = parent;
+        this.name = name;
+        this.attributes = attributes;
+        this.value = value;
+
+        if (parent != null) {
+            getParentList(parent).add(this);
+        }
+    }
+
+    private static List getParentList(Node parent) {
+        Object parentValue = parent.value();
+        List parentList;
+        if (parentValue instanceof List) {
+            parentList = (List) parentValue;
+        } else {
+            parentList = new NodeList();
+            parentList.add(parentValue);
+            parent.setValue(parentList);
+        }
+        return parentList;
+    }
+
+    /**
+     * Appends a child to the current node.
+     *
+     * @param child the child to append
+     * @return <code>true</code>
+     */
+    public boolean append(Node child) {
+        child.setParent(this);
+        return getParentList(this).add(child);
+    }
+
+    /**
+     * Removes a child of the current node.
+     *
+     * @param child the child to remove
+     * @return <code>true</code> if the param was a child of the current node
+     */
+    public boolean remove(Node child) {
+        child.setParent(null);
+        return getParentList(this).remove(child);
+    }
+
+    /**
+     * Creates a new node as a child of the current node.
+     *
+     * @param name the name of the new node
+     * @param attributes the attributes of the new node
+     * @return the newly created <code>Node</code>
+     */
+    public Node appendNode(Object name, Map attributes) {
+        return new Node(this, name, attributes);
+    }
+
+    /**
+     * Creates a new node as a child of the current node.
+     *
+     * @param name the name of the new node
+     * @return the newly created <code>Node</code>
+     */
+    public Node appendNode(Object name) {
+        return new Node(this, name);
+    }
+
+    /**
+     * Creates a new node as a child of the current node.
+     *
+     * @param name the name of the new node
+     * @param value the value of the new node
+     * @return the newly created <code>Node</code>
+     */
+    public Node appendNode(Object name, Object value) {
+        return new Node(this, name, value);
+    }
+
+    /**
+     * Creates a new node as a child of the current node.
+     *
+     * @param name the name of the new node
+     * @param attributes the attributes of the new node
+     * @param value the value of the new node
+     * @return the newly created <code>Node</code>
+     */
+    public Node appendNode(Object name, Map attributes, Object value) {
+        return new Node(this, name, attributes, value);
+    }
+
+    /**
+     * Replaces the current node with nodes defined using builder-style notation via a Closure.
+     *
+     * @param c A Closure defining the new nodes using builder-style notation.
+     * @return the original now replaced node
+     */
+    public Node replaceNode(Closure c) {
+        if (parent() == null) {
+            throw new UnsupportedOperationException("Replacing the root node is not supported");
+        }
+        appendNodes(c);
+        getParentList(parent()).remove(this);
+        this.setParent(null);
+        return this;
+    }
+
+    /**
+     * Replaces the current node with the supplied node.
+     *
+     * @param n the new Node
+     * @return the original now replaced node
+     */
+    public Node replaceNode(Node n) {
+        if (parent() == null) {
+            throw new UnsupportedOperationException("Replacing the root node is not supported");
+        }
+        List tail = getTail();
+        parent().appendNode(n.name(), n.attributes(), n.value());
+        parent().children().addAll(tail);
+        getParentList(parent()).remove(this);
+        this.setParent(null);
+        return this;
+    }
+
+    private List getTail() {
+        List list = parent().children();
+        int afterIndex = list.indexOf(this);
+        List tail = new ArrayList(list.subList(afterIndex + 1, list.size()));
+        list.subList(afterIndex + 1, list.size()).clear();
+        return tail;
+    }
+
+    /**
+     * Adds sibling nodes (defined using builder-style notation via a Closure) after the current node.
+     *
+     * @param c A Closure defining the new sibling nodes to add using builder-style notation.
+     */
+    public void plus(Closure c) {
+        if (parent() == null) {
+            throw new UnsupportedOperationException("Adding sibling nodes to the root node is not supported");
+        }
+        appendNodes(c);
+    }
+
+    private void appendNodes(Closure c) {
+        List tail = getTail();
+        for (Node child : buildChildrenFromClosure(c)) {
+            parent().appendNode(child.name(), child.attributes(), child.value());
+        }
+        parent().children().addAll(tail);
+    }
+
+    private static List<Node> buildChildrenFromClosure(Closure c) {
+        NodeBuilder b = new NodeBuilder();
+        Node newNode = (Node) b.invokeMethod("dummyNode", c);
+        return newNode.children();
+    }
+
+    /**
+     * Extension point for subclasses to override the metaclass. The default
+     * one supports the property and @ attribute notations.
+     *
+     * @param metaClass the original metaclass
+     * @param nodeClass the class whose metaclass we wish to override (this class or a subclass)
+     */
+    protected static void setMetaClass(final MetaClass metaClass, Class nodeClass) {
+        // TODO Is protected static a bit of a smell?
+        // TODO perhaps set nodeClass to be Class<? extends Node>
+        final MetaClass newMetaClass = new DelegatingMetaClass(metaClass) {
+            @Override
+            public Object getAttribute(final Object object, final String attribute) {
+                Node n = (Node) object;
+                return n.get("@" + attribute);
+            }
+
+            @Override
+            public void setAttribute(final Object object, final String attribute, final Object newValue) {
+                Node n = (Node) object;
+                n.attributes().put(attribute, newValue);
+            }
+
+            @Override
+            public Object getProperty(Object object, String property) {
+                if (object instanceof Node) {
+                    Node n = (Node) object;
+                    return n.get(property);
+                }
+                return super.getProperty(object, property);
+            }
+
+            @Override
+            public void setProperty(Object object, String property, Object newValue) {
+                if (property.startsWith("@")) {
+                    setAttribute(object, property.substring(1), newValue);
+                    return;
+                }
+                delegate.setProperty(object, property, newValue);
+            }
+
+        };
+        GroovySystem.getMetaClassRegistry().setMetaClass(nodeClass, newMetaClass);
+    }
+
+    /**
+     * Returns the textual representation of the current node and all its child nodes.
+     *
+     * @return the text value of the node including child text
+     */
+    public String text() {
+        if (value instanceof String) {
+            return (String) value;
+        }
+        if (value instanceof NodeList) {
+            return ((NodeList) value).text();
+        }
+        if (value instanceof Collection) {
+            Collection coll = (Collection) value;
+            String previousText = null;
+            StringBuilder sb = null;
+            for (Object child : coll) {
+                String childText = null;
+                if (child instanceof String) {
+                    childText = (String) child;
+                } else if (child instanceof Node) {
+                    childText = ((Node) child).text();
+                }
+                if (childText != null) {
+                    if (previousText == null) {
+                        previousText = childText;
+                    } else {
+                        if (sb == null) {
+                            sb = new StringBuilder();
+                            sb.append(previousText);
+                        }
+                        sb.append(childText);
+                    }
+                }
+            }
+            if (sb != null) {
+                return sb.toString();
+            } else {
+                if (previousText != null) {
+                    return previousText;
+                }
+                return "";
+            }
+        }
+        return "" + value;
+    }
+
+    /**
+     * Returns an <code>Iterator</code> of the children of the node.
+     *
+     * @return the iterator of the nodes children
+     */
+    public Iterator iterator() {
+        return children().iterator();
+    }
+
+    /**
+     * Returns a <code>List</code> of the nodes children.
+     *
+     * @return the nodes children
+     */
+    public List children() {
+        if (value == null) {
+            return new NodeList();
+        }
+        if (value instanceof List) {
+            return (List) value;
+        }
+        // we're probably just a String
+        List result = new NodeList();
+        result.add(value);
+        return result;
+    }
+
+    /**
+     * Returns a <code>Map</code> of the attributes of the node or an empty <code>Map</code>
+     * if the node does not have any attributes.
+     *
+     * @return the attributes of the node
+     */
+    public Map attributes() {
+        return attributes;
+    }
+
+    /**
+     * Provides lookup of attributes by key.
+     *
+     * @param key the key of interest
+     * @return the attribute matching the key or <code>null</code> if no match exists
+     */
+    public Object attribute(Object key) {
+        return (attributes != null) ? attributes.get(key) : null;
+    }
+
+    /**
+     * Returns an <code>Object</code> representing the name of the node.
+     *
+     * @return the name or <code>null</code> if name is empty
+     */
+    public Object name() {
+        return name;
+    }
+
+    /**
+     * Returns an <code>Object</code> representing the value of the node.
+     *
+     * @return the value or <code>null</code> if value is empty
+     */
+    public Object value() {
+        return value;
+    }
+
+    /**
+     * Adds or replaces the value of the node.
+     *
+     * @param value the new value of the node
+     */
+    public void setValue(Object value) {
+        this.value = value;
+    }
+
+    /**
+     * Returns the parent of the node.
+     *
+     * @return the parent or <code>null</code> for the root node
+     */
+    public Node parent() {
+        return parent;
+    }
+
+    /**
+     * Adds or replaces the parent of the node.
+     *
+     * @param parent the new parent of the node
+     */
+    protected void setParent(Node parent) {
+        this.parent = parent;
+    }
+
+    /**
+     * Provides lookup of elements by non-namespaced name
+     *
+     * @param key the name (or shortcut key) of the node(s) of interest
+     * @return the nodes which match key
+     */
+    public Object get(String key) {
+        if (key != null && key.charAt(0) == '@') {
+            String attributeName = key.substring(1);
+            return attributes().get(attributeName);
+        }
+        if ("..".equals(key)) {
+            return parent();
+        }
+        if ("*".equals(key)) {
+            return children();
+        }
+        if ("**".equals(key)) {
+            return depthFirst();
+        }
+        return getByName(key);
+    }
+
+    /**
+     * Provides lookup of elements by QName.
+     *
+     * @param name the QName of interest
+     * @return the nodes matching name
+     */
+    public NodeList getAt(QName name) {
+        NodeList answer = new NodeList();
+        for (Object child : children()) {
+            if (child instanceof Node) {
+                Node childNode = (Node) child;
+                Object childNodeName = childNode.name();
+                if (name.matches(childNodeName)) {
+                    answer.add(childNode);
+                }
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Provides lookup of elements by name.
+     *
+     * @param name the name of interest
+     * @return the nodes matching name
+     */
+    private NodeList getByName(String name) {
+        NodeList answer = new NodeList();
+        for (Object child : children()) {
+            if (child instanceof Node) {
+                Node childNode = (Node) child;
+                Object childNodeName = childNode.name();
+                if (childNodeName instanceof QName) {
+                    QName qn = (QName) childNodeName;
+                    if (qn.matches(name)) {
+                        answer.add(childNode);
+                    }
+                } else if (name.equals(childNodeName)) {
+                    answer.add(childNode);
+                }
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Provides a collection of all the nodes in the tree
+     * using a depth-first preorder traversal.
+     *
+     * @return the list of (depth-first) ordered nodes
+     */
+    public List depthFirst() {
+        return depthFirst(true);
+    }
+
+    /**
+     * Provides a collection of all the nodes in the tree
+     * using a depth-first traversal.
+     *
+     * @param preorder if false, a postorder depth-first traversal will be performed
+     * @return the list of (depth-first) ordered nodes
+     * @since 2.5.0
+     */
+    public List depthFirst(boolean preorder) {
+        List answer = new NodeList();
+        if (preorder) answer.add(this);
+        answer.addAll(depthFirstRest(preorder));
+        if (!preorder) answer.add(this);
+        return answer;
+    }
+
+    private List depthFirstRest(boolean preorder) {
+        List answer = new NodeList();
+        for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
+            Object child = iter.next();
+            if (child instanceof Node) {
+                Node childNode = (Node) child;
+                List children = childNode.depthFirstRest(preorder);
+                if (preorder) answer.add(childNode);
+                if (children.size() > 1 || (children.size() == 1 && !(children.get(0) instanceof String))) answer.addAll(children);
+                if (!preorder) answer.add(childNode);
+            } else if (child instanceof String) {
+                answer.add(child);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Provides a collection of all the nodes in the tree
+     * using a depth-first preorder traversal.
+     *
+     * @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
+     *          closure will be passed the node, for a two param closure the second parameter will be the level).
+     * @since 2.5.0
+     */
+    public void depthFirst(Closure c) {
+        Map<String, Object> options = new ListHashMap<String, Object>();
+        options.put("preorder", true);
+        depthFirst(options, c);
+    }
+
+    /**
+     * Provides a collection of all the nodes in the tree
+     * using a depth-first traversal.
+     * A boolean 'preorder' options is supported.
+     *
+     * @param options map containing options
+     * @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
+     *          closure will be passed the node, for a two param closure the second parameter will be the level).
+     * @since 2.5.0
+     */
+    public void depthFirst(Map<String, Object> options, Closure c) {
+        boolean preorder = Boolean.valueOf(options.get("preorder").toString());
+        if (preorder) callClosureForNode(c, this, 1);
+        depthFirstRest(preorder, 2, c);
+        if (!preorder) callClosureForNode(c, this, 1);
+    }
+
+    private static <T> T callClosureForNode(Closure<T> closure, Object node, int level) {
+        if (closure.getMaximumNumberOfParameters() == 2) {
+            return closure.call(new Object[]{node, level});
+        }
+        return closure.call(node);
+    }
+
+    private void depthFirstRest(boolean preorder, int level, Closure c) {
+        for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
+            Object child = iter.next();
+            if (child instanceof Node) {
+                Node childNode = (Node) child;
+                if (preorder) callClosureForNode(c, childNode, level);
+                childNode.depthFirstRest(preorder, level + 1, c);
+                if (!preorder) callClosureForNode(c, childNode, level);
+            }
+        }
+    }
+
+    /**
+     * Provides a collection of all the nodes in the tree
+     * using a breadth-first preorder traversal.
+     *
+     * @return the list of (breadth-first) ordered nodes
+     */
+    public List breadthFirst() {
+        return breadthFirst(true);
+    }
+
+    /**
+     * Provides a collection of all the nodes in the tree
+     * using a breadth-first traversal.
+     *
+     * @param preorder if false, a postorder breadth-first traversal will be performed
+     * @return the list of (breadth-first) ordered nodes
+     * @since 2.5.0
+     */
+    public List breadthFirst(boolean preorder) {
+        List answer = new NodeList();
+        if (preorder) answer.add(this);
+        answer.addAll(breadthFirstRest(preorder));
+        if (!preorder) answer.add(this);
+        return answer;
+    }
+
+    private List breadthFirstRest(boolean preorder) {
+        List answer = new NodeList();
+        Stack stack = new Stack();
+        List nextLevelChildren = preorder ? getDirectChildren() : DefaultGroovyMethods.reverse(getDirectChildren());
+        while (!nextLevelChildren.isEmpty()) {
+            List working = new NodeList(nextLevelChildren);
+            nextLevelChildren = new NodeList();
+            for (Object child : working) {
+                if (preorder) {
+                    answer.add(child);
+                } else {
+                    stack.push(child);
+                }
+                if (child instanceof Node) {
+                    Node childNode = (Node) child;
+                    List children = childNode.getDirectChildren();
+                    if (children.size() > 1 || (children.size() == 1 && !(children.get(0) instanceof String))) nextLevelChildren.addAll(preorder ? children : DefaultGroovyMethods.reverse(children));
+                }
+            }
+        }
+        while (!stack.isEmpty()) {
+            answer.add(stack.pop());
+        }
+        return answer;
+    }
+
+    /**
+     * Calls the provided closure for all the nodes in the tree
+     * using a breadth-first preorder traversal.
+     *
+     * @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
+     *          closure will be passed the node, for a two param closure the second parameter will be the level).
+     * @since 2.5.0
+     */
+    public void breadthFirst(Closure c) {
+        Map<String, Object> options = new ListHashMap<String, Object>();
+        options.put("preorder", true);
+        breadthFirst(options, c);
+    }
+
+    /**
+     * Calls the provided closure for all the nodes in the tree
+     * using a breadth-first traversal.
+     * A boolean 'preorder' options is supported.
+     *
+     * @param options map containing options
+     * @param c the closure to run for each node (a one or two parameter can be used; if one parameter is given the
+     *          closure will be passed the node, for a two param closure the second parameter will be the level).
+     * @since 2.5.0
+     */
+    public void breadthFirst(Map<String, Object> options, Closure c) {
+        boolean preorder = Boolean.valueOf(options.get("preorder").toString());
+        if (preorder) callClosureForNode(c, this, 1);
+        breadthFirstRest(preorder, 2, c);
+        if (!preorder) callClosureForNode(c, this, 1);
+    }
+
+    private void breadthFirstRest(boolean preorder, int level, Closure c) {
+        Stack<Tuple2<Object, Integer>> stack = new Stack<Tuple2<Object, Integer>>();
+        List nextLevelChildren = preorder ? getDirectChildren() : DefaultGroovyMethods.reverse(getDirectChildren());
+        while (!nextLevelChildren.isEmpty()) {
+            List working = new NodeList(nextLevelChildren);
+            nextLevelChildren = new NodeList();
+            for (Object child : working) {
+                if (preorder) {
+                    callClosureForNode(c, child, level);
+                } else {
+                    stack.push(new Tuple2<Object, Integer>(child, level));
+                }
+                if (child instanceof Node) {
+                    Node childNode = (Node) child;
+                    List children = childNode.getDirectChildren();
+                    if (children.size() > 1 || (children.size() == 1 && !(children.get(0) instanceof String))) nextLevelChildren.addAll(preorder ? children : DefaultGroovyMethods.reverse(children));
+                }
+            }
+            level++;
+        }
+        while (!stack.isEmpty()) {
+            Tuple2<Object, Integer> next = stack.pop();
+            callClosureForNode(c, next.getFirst(), next.getSecond());
+        }
+    }
+
+    /**
+     * Returns the list of any direct String nodes of this node.
+     *
+     * @return the list of String values from this node
+     * @since 2.3.0
+     */
+    public List<String> localText() {
+        List<String> answer = new ArrayList<String>();
+        for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
+            Object child = iter.next();
+            if (!(child instanceof Node)) {
+                answer.add(child.toString());
+            }
+        }
+        return answer;
+    }
+
+    private List getDirectChildren() {
+        List answer = new NodeList();
+        for (Iterator iter = InvokerHelper.asIterator(value); iter.hasNext(); ) {
+            Object child = iter.next();
+            if (child instanceof Node) {
+                Node childNode = (Node) child;
+                answer.add(childNode);
+            } else if (child instanceof String) {
+                answer.add(child);
+            }
+        }
+        return answer;
+    }
+
+    public String toString() {
+        return name + "[attributes=" + attributes + "; value=" + value + "]";
+    }
+
+    /**
+     * Writes the node to the specified <code>PrintWriter</code>.
+     *
+     * @param out the writer receiving the output
+     */
+    public void print(PrintWriter out) {
+        new NodePrinter(out).print(this);
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/NodeBuilder.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/NodeBuilder.java b/src/main/groovy/groovy/util/NodeBuilder.java
new file mode 100644
index 0000000..babe5c3
--- /dev/null
+++ b/src/main/groovy/groovy/util/NodeBuilder.java
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import java.util.Map;
+
+/**
+ * A helper class for creating nested trees of Node objects for 
+ * handling arbitrary data
+ * 
+ * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
+ */
+public class NodeBuilder extends BuilderSupport {
+
+    public static NodeBuilder newInstance() {
+        return new NodeBuilder();
+    }
+
+    protected void setParent(Object parent, Object child) {
+    }
+
+    protected Object createNode(Object name) {
+        return new Node(getCurrentNode(), name);
+    }
+
+    protected Object createNode(Object name, Object value) {
+        return new Node(getCurrentNode(), name, value);
+    }
+
+    protected Object createNode(Object name, Map attributes) {
+        return new Node(getCurrentNode(), name, attributes);
+    }
+
+    protected Object createNode(Object name, Map attributes, Object value) {
+        return new Node(getCurrentNode(), name, attributes, value);
+    }
+
+    protected Node getCurrentNode() {
+        return (Node) getCurrent();
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/NodeList.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/NodeList.java b/src/main/groovy/groovy/util/NodeList.java
new file mode 100644
index 0000000..e54009c
--- /dev/null
+++ b/src/main/groovy/groovy/util/NodeList.java
@@ -0,0 +1,202 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import groovy.lang.Closure;
+import groovy.lang.DelegatingMetaClass;
+import groovy.lang.GroovyRuntimeException;
+import groovy.lang.GroovySystem;
+import groovy.lang.MetaClass;
+import groovy.xml.QName;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A List implementation which is returned by queries on a {@link Node}
+ * which provides some XPath like helper methods for GPath.
+ *
+ * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
+ * @author Paul King
+ */
+public class NodeList extends ArrayList {
+    static {
+        // wrap the standard MetaClass with the delegate
+        setMetaClass(NodeList.class, GroovySystem.getMetaClassRegistry().getMetaClass(NodeList.class));
+    }
+
+    public NodeList() {
+    }
+
+    public NodeList(Collection collection) {
+        super(collection);
+    }
+
+    public NodeList(int size) {
+        super(size);
+    }
+
+    /**
+     * Creates a new NodeList containing the same elements as the
+     * original (but cloned in the case of Nodes).
+     *
+     * @return the clone
+     */
+    @Override
+    public Object clone() {
+        NodeList result = new NodeList(size());
+        for (int i = 0; i < size(); i++) {
+            Object next = get(i);
+            if (next instanceof Node) {
+                Node n = (Node) next;
+                result.add(n.clone());
+            } else {
+                result.add(next);
+            }
+        }
+        return result;
+    }
+
+    protected static void setMetaClass(final Class nodelistClass, final MetaClass metaClass) {
+        final MetaClass newMetaClass = new DelegatingMetaClass(metaClass) {
+            @Override
+            public Object getAttribute(final Object object, final String attribute) {
+                NodeList nl = (NodeList) object;
+                Iterator it = nl.iterator();
+                List result = new ArrayList();
+                while (it.hasNext()) {
+                    Node node = (Node) it.next();
+                    result.add(node.attributes().get(attribute));
+                }
+                return result;
+            }
+
+            @Override
+            public void setAttribute(final Object object, final String attribute, final Object newValue) {
+                for (Object o : (NodeList) object) {
+                    Node node = (Node) o;
+                    node.attributes().put(attribute, newValue);
+                }
+            }
+
+            @Override
+            public Object getProperty(Object object, String property) {
+                if (object instanceof NodeList) {
+                    NodeList nl = (NodeList) object;
+                    return nl.getAt(property);
+                }
+                return super.getProperty(object, property);
+            }
+        };
+        GroovySystem.getMetaClassRegistry().setMetaClass(nodelistClass, newMetaClass);
+    }
+
+    /**
+     * Provides lookup of elements by non-namespaced name.
+     *
+     * @param name the name or shortcut key for nodes of interest
+     * @return the nodes of interest which match name
+     */
+    public NodeList getAt(String name) {
+        NodeList answer = new NodeList();
+        for (Object child : this) {
+            if (child instanceof Node) {
+                Node childNode = (Node) child;
+                Object temp = childNode.get(name);
+                if (temp instanceof Collection) {
+                    answer.addAll((Collection) temp);
+                } else {
+                    answer.add(temp);
+                }
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Provides lookup of elements by QName.
+     *
+     * @param name the name or shortcut key for nodes of interest
+     * @return the nodes of interest which match name
+     */
+    public NodeList getAt(QName name) {
+        NodeList answer = new NodeList();
+        for (Object child : this) {
+            if (child instanceof Node) {
+                Node childNode = (Node) child;
+                NodeList temp = childNode.getAt(name);
+                answer.addAll(temp);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Returns the text value of all of the elements in the collection.
+     *
+     * @return the text value of all the elements in the collection or null
+     */
+    public String text() {
+        String previousText = null;
+        StringBuilder buffer = null;
+        for (Object child : this) {
+            String text = null;
+            if (child instanceof String) {
+                text = (String) child;
+            } else if (child instanceof Node) {
+                text = ((Node) child).text();
+            }
+            if (text != null) {
+                if (previousText == null) {
+                    previousText = text;
+                } else {
+                    if (buffer == null) {
+                        buffer = new StringBuilder();
+                        buffer.append(previousText);
+                    }
+                    buffer.append(text);
+                }
+            }
+        }
+        if (buffer != null) {
+            return buffer.toString();
+        }
+        if (previousText != null) {
+            return previousText;
+        }
+        return "";
+    }
+
+    public Node replaceNode(Closure c) {
+        if (size() <= 0 || size() > 1) {
+            throw new GroovyRuntimeException(
+                    "replaceNode() can only be used to replace a single node, but was applied to " + size() + " nodes");
+        }
+        return ((Node)get(0)).replaceNode(c);
+    }
+
+    public void plus(Closure c) {
+        for (Object o : this) {
+            ((Node) o).plus(c);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/NodePrinter.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/NodePrinter.java b/src/main/groovy/groovy/util/NodePrinter.java
new file mode 100644
index 0000000..ca93bc7
--- /dev/null
+++ b/src/main/groovy/groovy/util/NodePrinter.java
@@ -0,0 +1,130 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util;
+
+import org.codehaus.groovy.runtime.InvokerHelper;
+
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A helper class for creating nested trees of data
+ *
+ * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
+ * @author Christian Stein
+ */
+public class NodePrinter {
+
+    protected final IndentPrinter out;
+
+    public NodePrinter() {
+        this(new IndentPrinter(new PrintWriter(new OutputStreamWriter(System.out))));
+    }
+
+    public NodePrinter(PrintWriter out) {
+        this(new IndentPrinter(out));
+    }
+
+    public NodePrinter(IndentPrinter out) {
+        if (out == null) {
+            throw new NullPointerException("IndentPrinter 'out' must not be null!");
+        }
+        this.out = out;
+    }
+
+    public void print(Node node) {
+        out.printIndent();
+        printName(node);
+        Map attributes = node.attributes();
+        boolean hasAttributes = attributes != null && !attributes.isEmpty();
+        if (hasAttributes) {
+            printAttributes(attributes);
+        }
+        Object value = node.value();
+        if (value instanceof List) {
+            if (!hasAttributes) {
+                out.print("()");
+            }
+            printList((List) value);
+        } else {
+            if (value instanceof String) {
+                out.print("('");
+                out.print((String) value);
+                out.println("')");
+            } else {
+                out.println("()");
+            }
+        }
+        out.flush();
+    }
+
+    protected void printName(Node node) {
+        Object name = node.name();
+        if (name != null) {
+            out.print(name.toString());
+        } else {
+            out.print("null");
+        }
+    }
+
+    protected void printList(List list) {
+        if (list.isEmpty()) {
+            out.println("");
+        } else {
+            out.println(" {");
+            out.incrementIndent();
+            for (Object value : list) {
+                if (value instanceof Node) {
+                    print((Node) value);
+                } else {
+                    out.printIndent();
+                    out.println(InvokerHelper.toString(value));
+                }
+            }
+            out.decrementIndent();
+            out.printIndent();
+            out.println("}");
+        }
+    }
+
+
+    protected void printAttributes(Map attributes) {
+        out.print("(");
+        boolean first = true;
+        for (Object o : attributes.entrySet()) {
+            Map.Entry entry = (Map.Entry) o;
+            if (first) {
+                first = false;
+            } else {
+                out.print(", ");
+            }
+            out.print(entry.getKey().toString());
+            out.print(":");
+            if (entry.getValue() instanceof String) {
+                out.print("'" + entry.getValue() + "'");
+            } else {
+                out.print(InvokerHelper.toString(entry.getValue()));
+            }
+        }
+        out.print(")");
+    }
+
+}


Mime
View raw message