http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/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/10110145/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/10110145/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:[email protected]">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/10110145/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:[email protected]">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/10110145/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:[email protected]">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/10110145/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:[email protected]">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/10110145/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:[email protected]">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/10110145/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:[email protected]">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(")"); + } + +}
