http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/ui/GroovySocketServer.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/ui/GroovySocketServer.java b/src/main/groovy/groovy/ui/GroovySocketServer.java new file mode 100644 index 0000000..b0d27c5 --- /dev/null +++ b/src/main/groovy/groovy/ui/GroovySocketServer.java @@ -0,0 +1,226 @@ +/* + * 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.ui; + +import groovy.lang.GroovyCodeSource; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.GroovyShell; +import groovy.lang.Script; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.regex.Pattern; + +/** + * Simple server that executes supplied script against a socket. + * <p> + * Typically this is used from the groovy command line agent but it can be + * invoked programmatically. To run this program from the command line please + * refer to the command line documentation at + * <a href="http://docs.groovy-lang.org/docs/latest/html/documentation/#_running_groovy_from_the_commandline"> + * Running Groovy from the commandline</a>. + * <p> + * Here is an example of how to use this class to open a listening socket on the server, + * listen for incoming data, and then echo the data back to the client in reverse order: + * <pre> + * new GroovySocketServer( + * new GroovyShell(), // evaluator + * false, // is not a file + * "println line.reverse()", // script to evaluate + * true, // return result to client + * 1960) //port + * </pre> + * There are several variables in the script binding: + * <ul> + * <li>line - The data from the socket</li> + * <li>out - The output PrintWriter, should you need it for some reason.</li> + * <li>socket - The socket, should you need it for some reason.</li> + * </ul> + * + * @author Jeremy Rayner + */ +public class GroovySocketServer implements Runnable { + private URL url; + private final GroovyShell groovy; + private final GroovyCodeSource source; + private final boolean autoOutput; + private static int counter; + + /** + * This creates and starts the socket server on a new Thread. There is no need to call run or spawn + * a new thread yourself. + * @param groovy + * The GroovyShell object that evaluates the incoming text. If you need additional classes in the + * classloader then configure that through this object. + * @param isScriptFile + * Whether the incoming socket data String will be a script or a file path. + * @param scriptFilenameOrText + * This will be a groovy script or a file location depending on the argument isScriptFile. + * @param autoOutput + * whether output should be automatically echoed back to the client + * @param port + * the port to listen on + * + */ + public GroovySocketServer(GroovyShell groovy, boolean isScriptFile, String scriptFilenameOrText, boolean autoOutput, int port) { + this(groovy, getCodeSource(isScriptFile, scriptFilenameOrText), autoOutput, port); + } + + private static GroovyCodeSource getCodeSource(boolean scriptFile, String scriptFilenameOrText) { + if (scriptFile) { + try { + if (URI_PATTERN.matcher(scriptFilenameOrText).matches()) { + return new GroovyCodeSource(new URI(scriptFilenameOrText)); + } else { + return new GroovyCodeSource(GroovyMain.searchForGroovyScriptFile(scriptFilenameOrText)); + } + } catch (IOException e) { + throw new GroovyRuntimeException("Unable to get script from: " + scriptFilenameOrText, e); + } catch (URISyntaxException e) { + throw new GroovyRuntimeException("Unable to get script from URI: " + scriptFilenameOrText, e); + } + } else { + // We could jump through some hoops to have GroovyShell make our script name, but that seems unwarranted. + // If we *did* jump through that hoop then we should probably change the run loop to not recompile + // the script on every iteration since the script text can't change (the reason for the recompilation). + return new GroovyCodeSource(scriptFilenameOrText, generateScriptName(), GroovyShell.DEFAULT_CODE_BASE); + } + } + + private static synchronized String generateScriptName() { + return "ServerSocketScript" + (++counter) + ".groovy"; + } + + + // RFC2396 + // scheme = alpha *( alpha | digit | "+" | "-" | "." ) + private static final Pattern URI_PATTERN = Pattern.compile("\\p{Alpha}[-+.\\p{Alnum}]*:.*"); + + /** + * This creates and starts the socket server on a new Thread. There is no need to call run or spawn + * a new thread yourself. + * @param groovy + * The GroovyShell object that evaluates the incoming text. If you need additional classes in the + * classloader then configure that through this object. + * @param source + * GroovyCodeSource for the Groovy script + * @param autoOutput + * whether output should be automatically echoed back to the client + * @param port + * the port to listen on + * @since 2.3.0 + */ + public GroovySocketServer(GroovyShell groovy, GroovyCodeSource source, boolean autoOutput, int port) { + this.groovy = groovy; + this.source = source; + this.autoOutput = autoOutput; + try { + url = new URL("http", InetAddress.getLocalHost().getHostAddress(), port, "/"); + System.out.println("groovy is listening on port " + port); + } catch (IOException e) { + e.printStackTrace(); + } + new Thread(this).start(); + } + + /** + * Runs this server. There is typically no need to call this method, as the object's constructor + * creates a new thread and runs this object automatically. + */ + public void run() { + try { + ServerSocket serverSocket = new ServerSocket(url.getPort()); + while (true) { + // Create one script per socket connection. + // This is purposefully not caching the Script + // so that the script source file can be changed on the fly, + // as each connection is made to the server. + //FIXME: Groovy has other mechanisms specifically for watching to see if source code changes. + // We should probably be using that here. + // See also the comment about the fact we recompile a script that can't change. + Script script = groovy.parse(source); + new GroovyClientConnection(script, autoOutput, serverSocket.accept()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + static class GroovyClientConnection implements Runnable { + private Script script; + private Socket socket; + private BufferedReader reader; + private PrintWriter writer; + private boolean autoOutputFlag; + + GroovyClientConnection(Script script, boolean autoOutput,Socket socket) throws IOException { + this.script = script; + this.autoOutputFlag = autoOutput; + this.socket = socket; + reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + writer = new PrintWriter(socket.getOutputStream()); + new Thread(this, "Groovy client connection - " + socket.getInetAddress().getHostAddress()).start(); + } + public void run() { + try { + String line = null; + script.setProperty("out", writer); + script.setProperty("socket", socket); + script.setProperty("init", Boolean.TRUE); + while ((line = reader.readLine()) != null) { + // System.out.println(line); + script.setProperty("line", line); + Object o = script.run(); + script.setProperty("init", Boolean.FALSE); + if (o != null) { + if ("success".equals(o)) { + break; // to close sockets gracefully etc... + } else { + if (autoOutputFlag) { + writer.println(o); + } + } + } + writer.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + writer.flush(); + writer.close(); + } finally { + try { + socket.close(); + } catch (IOException e3) { + e3.printStackTrace(); + } + } + } + } + } +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/AbstractFactory.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/AbstractFactory.java b/src/main/groovy/groovy/util/AbstractFactory.java new file mode 100644 index 0000000..54e68e1 --- /dev/null +++ b/src/main/groovy/groovy/util/AbstractFactory.java @@ -0,0 +1,63 @@ +/* + * 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 java.util.Map; + +/** + * @author <a href="mailto:[email protected]">Andres Almiray</a> + * @author Danno Ferrin + */ +public abstract class AbstractFactory implements Factory { + public boolean isLeaf() { + return false; + } + + public boolean isHandlesNodeChildren() { + return false; + } + + public void onFactoryRegistration(FactoryBuilderSupport builder, String registeredName, String group) { + // do nothing + } + + public boolean onHandleNodeAttributes( FactoryBuilderSupport builder, Object node, + Map attributes ) { + return true; + } + + public boolean onNodeChildren( FactoryBuilderSupport builder, Object node, Closure childContent) { + return true; + } + + public void onNodeCompleted( FactoryBuilderSupport builder, Object parent, Object node ) { + // do nothing + } + + public void setParent( FactoryBuilderSupport builder, Object parent, Object child ) { + // do nothing + } + + public void setChild( FactoryBuilderSupport builder, Object parent, Object child ) { + // do nothing + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/BufferedIterator.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/BufferedIterator.java b/src/main/groovy/groovy/util/BufferedIterator.java new file mode 100644 index 0000000..6fa50a9 --- /dev/null +++ b/src/main/groovy/groovy/util/BufferedIterator.java @@ -0,0 +1,31 @@ +/* + * 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.Iterator; + +/** + * An iterator that allows examining the next element without consuming it. + * + * @author Andrew Taylor + * @since 2.5.0 + */ +public interface BufferedIterator<T> extends Iterator<T> { + T head(); +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/BuilderSupport.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/BuilderSupport.java b/src/main/groovy/groovy/util/BuilderSupport.java new file mode 100644 index 0000000..f634f1f --- /dev/null +++ b/src/main/groovy/groovy/util/BuilderSupport.java @@ -0,0 +1,228 @@ +/* + * 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.GroovyObjectSupport; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.MissingMethodException; +import org.codehaus.groovy.runtime.InvokerHelper; + +import java.util.List; +import java.util.Map; + +/** + * An abstract base class for creating arbitrary nested trees of objects + * or events + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public abstract class BuilderSupport extends GroovyObjectSupport { + + private Object current; + private Closure nameMappingClosure; + private final BuilderSupport proxyBuilder; + + public BuilderSupport() { + this.proxyBuilder = this; + } + + public BuilderSupport(BuilderSupport proxyBuilder) { + this(null, proxyBuilder); + } + + public BuilderSupport(Closure nameMappingClosure, BuilderSupport proxyBuilder) { + this.nameMappingClosure = nameMappingClosure; + this.proxyBuilder = proxyBuilder; + } + + /** + * Convenience method when no arguments are required + * + * @param methodName the name of the method to invoke + * @return the result of the call + */ + public Object invokeMethod(String methodName) { + return invokeMethod(methodName, null); + } + + public Object invokeMethod(String methodName, Object args) { + Object name = getName(methodName); + return doInvokeMethod(methodName, name, args); + } + + protected Object doInvokeMethod(String methodName, Object name, Object args) { + Object node = null; + Closure closure = null; + List list = InvokerHelper.asList(args); + + //System.out.println("Called invokeMethod with name: " + name + " arguments: " + list); + + switch (list.size()) { + case 0: + node = proxyBuilder.createNode(name); + break; + case 1: { + Object object = list.get(0); + if (object instanceof Map) { + node = proxyBuilder.createNode(name, (Map) object); + } else if (object instanceof Closure) { + closure = (Closure) object; + node = proxyBuilder.createNode(name); + } else { + node = proxyBuilder.createNode(name, object); + } + } + break; + case 2: { + Object object1 = list.get(0); + Object object2 = list.get(1); + if (object1 instanceof Map) { + if (object2 instanceof Closure) { + closure = (Closure) object2; + node = proxyBuilder.createNode(name, (Map) object1); + } else { + node = proxyBuilder.createNode(name, (Map) object1, object2); + } + } else { + if (object2 instanceof Closure) { + closure = (Closure) object2; + node = proxyBuilder.createNode(name, object1); + } else if (object2 instanceof Map) { + node = proxyBuilder.createNode(name, (Map) object2, object1); + } else { + throw new MissingMethodException(name.toString(), getClass(), list.toArray(), false); + } + } + } + break; + case 3: { + Object arg0 = list.get(0); + Object arg1 = list.get(1); + Object arg2 = list.get(2); + if (arg0 instanceof Map && arg2 instanceof Closure) { + closure = (Closure) arg2; + node = proxyBuilder.createNode(name, (Map) arg0, arg1); + } else if (arg1 instanceof Map && arg2 instanceof Closure) { + closure = (Closure) arg2; + node = proxyBuilder.createNode(name, (Map) arg1, arg0); + } else { + throw new MissingMethodException(name.toString(), getClass(), list.toArray(), false); + } + } + break; + default: { + throw new MissingMethodException(name.toString(), getClass(), list.toArray(), false); + } + + } + + if (current != null) { + proxyBuilder.setParent(current, node); + } + + if (closure != null) { + // push new node on stack + Object oldCurrent = getCurrent(); + setCurrent(node); + // let's register the builder as the delegate + setClosureDelegate(closure, node); + try { + closure.call(); + } catch (Exception e) { + throw new GroovyRuntimeException(e); + } + setCurrent(oldCurrent); + } + + proxyBuilder.nodeCompleted(current, node); + return proxyBuilder.postNodeCompletion(current, node); + } + + /** + * A strategy method to allow derived builders to use + * builder-trees and switch in different kinds of builders. + * This method should call the setDelegate() method on the closure + * which by default passes in this but if node is-a builder + * we could pass that in instead (or do something wacky too) + * + * @param closure the closure on which to call setDelegate() + * @param node the node value that we've just created, which could be + * a builder + */ + protected void setClosureDelegate(Closure closure, Object node) { + closure.setDelegate(this); + } + + protected abstract void setParent(Object parent, Object child); + + protected abstract Object createNode(Object name); + + protected abstract Object createNode(Object name, Object value); + + protected abstract Object createNode(Object name, Map attributes); + + protected abstract Object createNode(Object name, Map attributes, Object value); + + /** + * A hook to allow names to be converted into some other object + * such as a QName in XML or ObjectName in JMX. + * + * @param methodName the name of the desired method + * @return the object representing the name + */ + protected Object getName(String methodName) { + if (nameMappingClosure != null) { + return nameMappingClosure.call(methodName); + } + return methodName; + } + + + /** + * A hook to allow nodes to be processed once they have had all of their + * children applied. + * + * @param node the current node being processed + * @param parent the parent of the node being processed + */ + protected void nodeCompleted(Object parent, Object node) { + } + + /** + * A hook to allow nodes to be processed once they have had all of their + * children applied and allows the actual node object that represents + * the Markup element to be changed + * + * @param node the current node being processed + * @param parent the parent of the node being processed + * @return the node, possibly new, that represents the markup element + */ + protected Object postNodeCompletion(Object parent, Object node) { + return node; + } + + protected Object getCurrent() { + return current; + } + + protected void setCurrent(Object current) { + this.current = current; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/CharsetToolkit.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/CharsetToolkit.java b/src/main/groovy/groovy/util/CharsetToolkit.java new file mode 100644 index 0000000..e127459 --- /dev/null +++ b/src/main/groovy/groovy/util/CharsetToolkit.java @@ -0,0 +1,419 @@ +/* + * 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.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.nio.charset.Charset; +import java.util.Collection; + +/** + * Utility class to guess the encoding of a given text file. + * <p> + * Unicode files encoded in UTF-16 (low or big endian) or UTF-8 files + * with a Byte Order Marker are correctly discovered. For UTF-8 files with no BOM, if the buffer + * is wide enough, the charset should also be discovered. + * <p> + * A byte buffer of 4KB is used to be able to guess the encoding. + * <p> + * Usage: + * <pre> + * CharsetToolkit toolkit = new CharsetToolkit(file); + * + * // guess the encoding + * Charset guessedCharset = toolkit.getCharset(); + * + * // create a reader with the correct charset + * BufferedReader reader = toolkit.getReader(); + * + * // read the file content + * String line; + * while ((line = br.readLine())!= null) + * { + * System.out.println(line); + * } + * </pre> + * + * @author Guillaume Laforge + */ +public class CharsetToolkit { + private final byte[] buffer; + private Charset defaultCharset; + private Charset charset; + private boolean enforce8Bit = true; + private final File file; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * Constructor of the <code>CharsetToolkit</code> utility class. + * + * @param file of which we want to know the encoding. + */ + public CharsetToolkit(File file) throws IOException { + this.file = file; + this.defaultCharset = getDefaultSystemCharset(); + this.charset = null; + InputStream input = new FileInputStream(file); + try { + byte[] bytes = new byte[4096]; + int bytesRead = input.read(bytes); + if (bytesRead == -1) { + this.buffer = EMPTY_BYTE_ARRAY; + } + else if (bytesRead < 4096) { + byte[] bytesToGuess = new byte[bytesRead]; + System.arraycopy(bytes, 0, bytesToGuess, 0, bytesRead); + this.buffer = bytesToGuess; + } + else { + this.buffer = bytes; + } + } finally { + try {input.close();} catch (IOException e){ + // IGNORE + } + } + } + + /** + * Defines the default <code>Charset</code> used in case the buffer represents + * an 8-bit <code>Charset</code>. + * + * @param defaultCharset the default <code>Charset</code> to be returned + * if an 8-bit <code>Charset</code> is encountered. + */ + public void setDefaultCharset(Charset defaultCharset) { + if (defaultCharset != null) + this.defaultCharset = defaultCharset; + else + this.defaultCharset = getDefaultSystemCharset(); + } + + public Charset getCharset() { + if (this.charset == null) + this.charset = guessEncoding(); + return charset; + } + + /** + * If US-ASCII is recognized, enforce to return the default encoding, rather than US-ASCII. + * It might be a file without any special character in the range 128-255, but that may be or become + * a file encoded with the default <code>charset</code> rather than US-ASCII. + * + * @param enforce a boolean specifying the use or not of US-ASCII. + */ + public void setEnforce8Bit(boolean enforce) { + this.enforce8Bit = enforce; + } + + /** + * Gets the enforce8Bit flag, in case we do not want to ever get a US-ASCII encoding. + * + * @return a boolean representing the flag of use of US-ASCII. + */ + public boolean getEnforce8Bit() { + return this.enforce8Bit; + } + + /** + * Retrieves the default Charset + */ + public Charset getDefaultCharset() { + return defaultCharset; + } + + /** + * Guess the encoding of the provided buffer. + * If Byte Order Markers are encountered at the beginning of the buffer, we immediately + * return the charset implied by this BOM. Otherwise, the file would not be a human + * readable text file. + * <p> + * If there is no BOM, this method tries to discern whether the file is UTF-8 or not. + * If it is not UTF-8, we assume the encoding is the default system encoding + * (of course, it might be any 8-bit charset, but usually, an 8-bit charset is the default one). + * <p> + * It is possible to discern UTF-8 thanks to the pattern of characters with a multi-byte sequence. + * <pre> + * UCS-4 range (hex.) UTF-8 octet sequence (binary) + * 0000 0000-0000 007F 0xxxxxxx + * 0000 0080-0000 07FF 110xxxxx 10xxxxxx + * 0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx + * 0001 0000-001F FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + * 0020 0000-03FF FFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + * 0400 0000-7FFF FFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + * </pre> + * With UTF-8, 0xFE and 0xFF never appear. + * + * @return the Charset recognized. + */ + private Charset guessEncoding() { + // if the file has a Byte Order Marker, we can assume the file is in UTF-xx + // otherwise, the file would not be human readable + if (hasUTF8Bom()) + return Charset.forName("UTF-8"); + if (hasUTF16LEBom()) + return Charset.forName("UTF-16LE"); + if (hasUTF16BEBom()) + return Charset.forName("UTF-16BE"); + + // if a byte has its most significant bit set, the file is in UTF-8 or in the default encoding + // otherwise, the file is in US-ASCII + boolean highOrderBit = false; + + // if the file is in UTF-8, high order bytes must have a certain value, in order to be valid + // if it's not the case, we can assume the encoding is the default encoding of the system + boolean validU8Char = true; + + // TODO the buffer is not read up to the end, but up to length - 6 + + int length = buffer.length; + int i = 0; + while (i < length - 6) { + byte b0 = buffer[i]; + byte b1 = buffer[i + 1]; + byte b2 = buffer[i + 2]; + byte b3 = buffer[i + 3]; + byte b4 = buffer[i + 4]; + byte b5 = buffer[i + 5]; + if (b0 < 0) { + // a high order bit was encountered, thus the encoding is not US-ASCII + // it may be either an 8-bit encoding or UTF-8 + highOrderBit = true; + // a two-bytes sequence was encountered + if (isTwoBytesSequence(b0)) { + // there must be one continuation byte of the form 10xxxxxx, + // otherwise the following character is is not a valid UTF-8 construct + if (!isContinuationChar(b1)) + validU8Char = false; + else + i++; + } + // a three-bytes sequence was encountered + else if (isThreeBytesSequence(b0)) { + // there must be two continuation bytes of the form 10xxxxxx, + // otherwise the following character is is not a valid UTF-8 construct + if (!(isContinuationChar(b1) && isContinuationChar(b2))) + validU8Char = false; + else + i += 2; + } + // a four-bytes sequence was encountered + else if (isFourBytesSequence(b0)) { + // there must be three continuation bytes of the form 10xxxxxx, + // otherwise the following character is is not a valid UTF-8 construct + if (!(isContinuationChar(b1) && isContinuationChar(b2) && isContinuationChar(b3))) + validU8Char = false; + else + i += 3; + } + // a five-bytes sequence was encountered + else if (isFiveBytesSequence(b0)) { + // there must be four continuation bytes of the form 10xxxxxx, + // otherwise the following character is is not a valid UTF-8 construct + if (!(isContinuationChar(b1) + && isContinuationChar(b2) + && isContinuationChar(b3) + && isContinuationChar(b4))) + validU8Char = false; + else + i += 4; + } + // a six-bytes sequence was encountered + else if (isSixBytesSequence(b0)) { + // there must be five continuation bytes of the form 10xxxxxx, + // otherwise the following character is is not a valid UTF-8 construct + if (!(isContinuationChar(b1) + && isContinuationChar(b2) + && isContinuationChar(b3) + && isContinuationChar(b4) + && isContinuationChar(b5))) + validU8Char = false; + else + i += 5; + } + else + validU8Char = false; + } + if (!validU8Char) + break; + i++; + } + // if no byte with an high order bit set, the encoding is US-ASCII + // (it might have been UTF-7, but this encoding is usually internally used only by mail systems) + if (!highOrderBit) { + // returns the default charset rather than US-ASCII if the enforce8Bit flag is set. + if (this.enforce8Bit) + return this.defaultCharset; + else + return Charset.forName("US-ASCII"); + } + // if no invalid UTF-8 were encountered, we can assume the encoding is UTF-8, + // otherwise the file would not be human readable + if (validU8Char) + return Charset.forName("UTF-8"); + // finally, if it's not UTF-8 nor US-ASCII, let's assume the encoding is the default encoding + return this.defaultCharset; + } + + /** + * If the byte has the form 10xxxxx, then it's a continuation byte of a multiple byte character; + * + * @param b a byte. + * @return true if it's a continuation char. + */ + private static boolean isContinuationChar(byte b) { + return -128 <= b && b <= -65; + } + + /** + * If the byte has the form 110xxxx, then it's the first byte of a two-bytes sequence character. + * + * @param b a byte. + * @return true if it's the first byte of a two-bytes sequence. + */ + private static boolean isTwoBytesSequence(byte b) { + return -64 <= b && b <= -33; + } + + /** + * If the byte has the form 1110xxx, then it's the first byte of a three-bytes sequence character. + * + * @param b a byte. + * @return true if it's the first byte of a three-bytes sequence. + */ + private static boolean isThreeBytesSequence(byte b) { + return -32 <= b && b <= -17; + } + + /** + * If the byte has the form 11110xx, then it's the first byte of a four-bytes sequence character. + * + * @param b a byte. + * @return true if it's the first byte of a four-bytes sequence. + */ + private static boolean isFourBytesSequence(byte b) { + return -16 <= b && b <= -9; + } + + /** + * If the byte has the form 11110xx, then it's the first byte of a five-bytes sequence character. + * + * @param b a byte. + * @return true if it's the first byte of a five-bytes sequence. + */ + private static boolean isFiveBytesSequence(byte b) { + return -8 <= b && b <= -5; + } + + /** + * If the byte has the form 1110xxx, then it's the first byte of a six-bytes sequence character. + * + * @param b a byte. + * @return true if it's the first byte of a six-bytes sequence. + */ + private static boolean isSixBytesSequence(byte b) { + return -4 <= b && b <= -3; + } + + /** + * Retrieve the default charset of the system. + * + * @return the default <code>Charset</code>. + */ + public static Charset getDefaultSystemCharset() { + return Charset.forName(System.getProperty("file.encoding")); + } + + /** + * Has a Byte Order Marker for UTF-8 (Used by Microsoft's Notepad and other editors). + * + * @return true if the buffer has a BOM for UTF8. + */ + public boolean hasUTF8Bom() { + if (buffer.length >= 3) + return (buffer[0] == -17 && buffer[1] == -69 && buffer[2] == -65); + else + return false; + } + + /** + * Has a Byte Order Marker for UTF-16 Low Endian + * (ucs-2le, ucs-4le, and ucs-16le). + * + * @return true if the buffer has a BOM for UTF-16 Low Endian. + */ + public boolean hasUTF16LEBom() { + if (buffer.length >= 2) + return (buffer[0] == -1 && buffer[1] == -2); + else + return false; + } + + /** + * Has a Byte Order Marker for UTF-16 Big Endian + * (utf-16 and ucs-2). + * + * @return true if the buffer has a BOM for UTF-16 Big Endian. + */ + public boolean hasUTF16BEBom() { + if (buffer.length >= 2) + return (buffer[0] == -2 && buffer[1] == -1); + else + return false; + } + + /** + * Gets a <code>BufferedReader</code> (indeed a <code>LineNumberReader</code>) from the <code>File</code> + * specified in the constructor of <code>CharsetToolkit</code> using the charset discovered or the default + * charset if an 8-bit <code>Charset</code> is encountered. + * + * @return a <code>BufferedReader</code> + * @throws FileNotFoundException if the file is not found. + */ + public BufferedReader getReader() throws FileNotFoundException { + LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(file), getCharset())); + if (hasUTF8Bom() || hasUTF16LEBom() || hasUTF16BEBom()) { + try { + reader.read(); + } + catch (IOException e) { + // should never happen, as a file with no content + // but with a BOM has at least one char + } + } + return reader; + } + + /** + * Retrieves all the available <code>Charset</code>s on the platform, + * among which the default <code>charset</code>. + * + * @return an array of <code>Charset</code>s. + */ + public static Charset[] getAvailableCharsets() { + Collection collection = Charset.availableCharsets().values(); + return (Charset[]) collection.toArray(new Charset[collection.size()]); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/CliBuilder.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/CliBuilder.groovy b/src/main/groovy/groovy/util/CliBuilder.groovy new file mode 100644 index 0000000..bc7d44a --- /dev/null +++ b/src/main/groovy/groovy/util/CliBuilder.groovy @@ -0,0 +1,798 @@ +/* + * 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.cli.CliBuilderException +import groovy.cli.Option +import groovy.cli.TypedOption +import groovy.cli.Unparsed +import groovy.transform.Undefined +import org.apache.commons.cli.CommandLine +import org.apache.commons.cli.CommandLineParser +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.GnuParser +import org.apache.commons.cli.HelpFormatter +import org.apache.commons.cli.Option as CliOption +import org.apache.commons.cli.Options +import org.apache.commons.cli.ParseException +import org.codehaus.groovy.runtime.InvokerHelper +import org.codehaus.groovy.runtime.MetaClassHelper +import org.codehaus.groovy.runtime.StringGroovyMethods + +import java.lang.annotation.Annotation +import java.lang.reflect.Array +import java.lang.reflect.Field +import java.lang.reflect.Method + +/** + * Provides a builder to assist the processing of command line arguments. + * Two styles are supported: dynamic api style (declarative method calls provide a mini DSL for describing options) + * and annotation style (annotations on an interface or class describe options). + * <p> + * <b>Dynamic api style</b> + * <p> + * Typical usage (emulate partial arg processing of unix command: ls -alt *.groovy): + * <pre> + * def cli = new CliBuilder(usage:'ls') + * cli.a('display all files') + * cli.l('use a long listing format') + * cli.t('sort by modification time') + * def options = cli.parse(args) + * assert options // would be null (false) on failure + * assert options.arguments() == ['*.groovy'] + * assert options.a && options.l && options.t + * </pre> + * The usage message for this example (obtained using <code>cli.usage()</code>) is shown below: + * <pre> + * usage: ls + * -a display all files + * -l use a long listing format + * -t sort by modification time + * </pre> + * An underlying parser that supports what is called argument 'bursting' is used + * by default. Bursting would convert '-alt' into '-a -l -t' provided no long + * option exists with value 'alt' and provided that none of 'a', 'l' or 't' + * takes an argument (in fact the last one is allowed to take an argument). + * The bursting behavior can be turned off by using an + * alternate underlying parser. The simplest way to achieve this is by using + * the deprecated GnuParser from Commons CLI with the parser property on the CliBuilder, + * i.e. include <code>parser: new GnuParser()</code> in the constructor call. + * <p> + * Another example (partial emulation of arg processing for 'ant' command line): + * <pre> + * def cli = new CliBuilder(usage:'ant [options] [targets]', + * header:'Options:') + * cli.help('print this message') + * cli.logfile(args:1, argName:'file', 'use given file for log') + * cli.D(args:2, valueSeparator:'=', argName:'property=value', + * 'use value for given property') + * def options = cli.parse(args) + * ... + * </pre> + * Usage message would be: + * <pre> + * usage: ant [options] [targets] + * Options: + * -D <property=value> use value for given property + * -help print this message + * -logfile <file> use given file for log + * </pre> + * And if called with the following arguments '-logfile foo -Dbar=baz target' + * then the following assertions would be true: + * <pre> + * assert options // would be null (false) on failure + * assert options.arguments() == ['target'] + * assert options.Ds == ['bar', 'baz'] + * assert options.logfile == 'foo' + * </pre> + * Note the use of some special notation. By adding 's' onto an option + * that may appear multiple times and has an argument or as in this case + * uses a valueSeparator to separate multiple argument values + * causes the list of associated argument values to be returned. + * <p> + * Another example showing long options (partial emulation of arg processing for 'curl' command line): + * <pre> + * def cli = new CliBuilder(usage:'curl [options] <url>') + * cli._(longOpt:'basic', 'Use HTTP Basic Authentication') + * cli.d(longOpt:'data', args:1, argName:'data', 'HTTP POST data') + * cli.G(longOpt:'get', 'Send the -d data with a HTTP GET') + * cli.q('If used as the first parameter disables .curlrc') + * cli._(longOpt:'url', args:1, argName:'URL', 'Set URL to work with') + * </pre> + * Which has the following usage message: + * <pre> + * usage: curl [options] <url> + * --basic Use HTTP Basic Authentication + * -d,--data <data> HTTP POST data + * -G,--get Send the -d data with a HTTP GET + * -q If used as the first parameter disables .curlrc + * --url <URL> Set URL to work with + * </pre> + * This example shows a common convention. When mixing short and long names, the + * short names are often one character in size. One character options with + * arguments don't require a space between the option and the argument, e.g. + * <code>-Ddebug=true</code>. The example also shows + * the use of '_' when no short option is applicable. + * <p> + * Also note that '_' was used multiple times. This is supported but if + * any other shortOpt or any longOpt is repeated, then the behavior is undefined. + * <p> + * Short option names may not contain a hyphen. If a long option name contains a hyphen, e.g. '--max-wait' then you can either + * use the long hand method call <code>options.hasOption('max-wait')</code> or surround + * the option name in quotes, e.g. <code>options.'max-wait'</code>. + * <p> + * Although CliBuilder on the whole hides away the underlying library used + * for processing the arguments, it does provide some hooks which let you + * make use of the underlying library directly should the need arise. For + * example, the last two lines of the 'curl' example above could be replaced + * with the following: + * <pre> + * import org.apache.commons.cli.* + * ... as before ... + * cli << new Option('q', false, 'If used as the first parameter disables .curlrc') + * cli << Option.builder().longOpt('url').hasArg().argName('URL'). + * desc('Set URL to work with').build() + * ... + * </pre> + * + * CliBuilder also supports Argument File processing. If an argument starts with + * an '@' character followed by a filename, then the contents of the file with name + * filename are placed into the command line. The feature can be turned off by + * setting expandArgumentFiles to false. If turned on, you can still pass a real + * parameter with an initial '@' character by escaping it with an additional '@' + * symbol, e.g. '@@foo' will become '@foo' and not be subject to expansion. As an + * example, if the file temp.args contains the content: + * <pre> + * -arg1 + * paramA + * paramB paramC + * </pre> + * Then calling the command line with: + * <pre> + * someCommand @temp.args -arg2 paramD + * </pre> + * Is the same as calling this: + * <pre> + * someCommand -arg1 paramA paramB paramC -arg2 paramD + * </pre> + * This feature is particularly useful on operating systems which place limitations + * on the size of the command line (e.g. Windows). The feature is similar to + * the 'Command Line Argument File' processing supported by javadoc and javac. + * Consult the corresponding documentation for those tools if you wish to see further examples. + * <p> + * <b>Supported Option Properties</b>: + * <pre> + * argName: String + * longOpt: String + * args: int or String + * optionalArg: boolean + * required: boolean + * type: Class + * valueSeparator: char + * convert: Closure + * defaultValue: String + * </pre> + * See {@link org.apache.commons.cli.Option} for the meaning of most of these properties + * and {@link CliBuilderTest} for further examples. + * <p> + * <b>Annotation style with an interface</b> + * <p> + * With this style an interface is defined containing an annotated method for each option. + * It might look like this (following roughly the earlier 'ls' example): + * <pre> + * import groovy.cli.Option + * import groovy.cli.Unparsed + * + * interface OptionInterface { + * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all() + * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat() + * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time() + * @{@link groovy.cli.Unparsed} List remaining() + * } + * </pre> + * Then this description is supplied to CliBuilder during parsing, e.g.: + * <pre> + * def args = '-alt *.groovy'.split() // normally from commandline itself + * def cli = new CliBuilder(usage:'ls') + * def options = cli.parseFromSpec(OptionInterface, args) + * assert options.remaining() == ['*.groovy'] + * assert options.all() && options.longFormat() && options.time() + * </pre> + * <p> + * <b>Annotation style with a class</b> + * <p> + * With this style a user-supplied instance is used. Annotations on that instance's class + * members (properties and setter methods) indicate how to set options and provide the option details + * using annotation attributes. + * It might look like this (again using the earlier 'ls' example): + * <pre> + * import groovy.cli.Option + * import groovy.cli.Unparsed + * + * class OptionClass { + * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all + * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat + * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time + * @{@link groovy.cli.Unparsed} List remaining + * } + * </pre> + * Then this description is supplied to CliBuilder during parsing, e.g.: + * <pre> + * def args = '-alt *.groovy'.split() // normally from commandline itself + * def cli = new CliBuilder(usage:'ls') + * def options = new OptionClass() + * cli.parseFromInstance(options, args) + * assert options.remaining == ['*.groovy'] + * assert options.all && options.longFormat && options.time + * </pre> + */ +class CliBuilder { + + /** + * Usage summary displayed as the first line when <code>cli.usage()</code> is called. + */ + String usage = 'groovy' + + /** + * Normally set internally but allows you full customisation of the underlying processing engine. + */ + CommandLineParser parser = null + + /** + * To change from the default PosixParser to the GnuParser, set this to false. Ignored if the parser is explicitly set. + * @deprecated use the parser option instead with an instance of your preferred parser + */ + @Deprecated + Boolean posix = null + + /** + * Whether arguments of the form '{@code @}<i>filename</i>' will be expanded into the arguments contained within the file named <i>filename</i> (default true). + */ + boolean expandArgumentFiles = true + + /** + * Normally set internally but can be overridden if you want to customise how the usage message is displayed. + */ + HelpFormatter formatter = new HelpFormatter() + + /** + * Defaults to stdout but you can provide your own PrintWriter if desired. + */ + PrintWriter writer = new PrintWriter(System.out) + + /** + * Optional additional message for usage; displayed after the usage summary but before the options are displayed. + */ + String header = '' + + /** + * Optional additional message for usage; displayed after the options are displayed. + */ + String footer = '' + + /** + * Indicates that option processing should continue for all arguments even + * if arguments not recognized as options are encountered (default true). + */ + boolean stopAtNonOption = true + + /** + * Allows customisation of the usage message width. + */ + int width = HelpFormatter.DEFAULT_WIDTH + + /** + * Not normally accessed directly but full access to underlying options if needed. + */ + Options options = new Options() + + Map<String, TypedOption> savedTypeOptions = new HashMap<String, TypedOption>() + + public <T> TypedOption<T> option(Map args, Class<T> type, String description) { + def name = args.opt ?: '_' + args.type = type + args.remove('opt') + "$name"(args, description) + } + + /** + * Internal method: Detect option specification method calls. + */ + def invokeMethod(String name, Object args) { + if (args instanceof Object[]) { + if (args.size() == 1 && (args[0] instanceof String || args[0] instanceof GString)) { + def option = option(name, [:], args[0]) + options.addOption(option) + + return create(option, null, null, null) + } + if (args.size() == 1 && args[0] instanceof CliOption && name == 'leftShift') { + CliOption option = args[0] + options.addOption(option) + return create(option, null, null, null) + } + if (args.size() == 2 && args[0] instanceof Map) { + def convert = args[0].remove('convert') + def type = args[0].remove('type') + def defaultValue = args[0].remove('defaultValue') + if (type && !(type instanceof Class)) { + throw new CliBuilderException("'type' must be a Class") + } + if ((convert || type) && !args[0].containsKey('args') && + type?.simpleName?.toLowerCase() != 'boolean') { + args[0].args = 1 + } + def option = option(name, args[0], args[1]) + options.addOption(option) + return create(option, type, defaultValue, convert) + } + } + return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args) + } + + /** + * Make options accessible from command line args with parser. + * Returns null on bad command lines after displaying usage message. + */ + OptionAccessor parse(args) { + if (expandArgumentFiles) args = expandArgumentFiles(args) + if (!parser) { + parser = posix != null && posix == false ? new GnuParser() : new DefaultParser() + } + try { + def accessor = new OptionAccessor( + parser.parse(options, args as String[], stopAtNonOption)) + accessor.savedTypeOptions = savedTypeOptions + return accessor + } catch (ParseException pe) { + writer.println("error: " + pe.message) + usage() + return null + } + } + + /** + * Print the usage message with writer (default: System.out) and formatter (default: HelpFormatter) + */ + void usage() { + formatter.printHelp(writer, width, usage, header, options, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, footer) + writer.flush() + } + + /** + * Given an interface containing members with annotations, derive + * the options specification. + * + * @param optionsClass + * @param args + * @return an instance containing the processed options + */ + public <T> T parseFromSpec(Class<T> optionsClass, String[] args) { + addOptionsFromAnnotations(optionsClass, false) + def cli = parse(args) + def cliOptions = [:] + setOptionsFromAnnotations(cli, optionsClass, cliOptions, false) + cliOptions as T + } + + /** + * Given an instance containing members with annotations, derive + * the options specification. + * + * @param optionInstance + * @param args + * @return the options instance populated with the processed options + */ + public <T> T parseFromInstance(T optionInstance, args) { + addOptionsFromAnnotations(optionInstance.getClass(), true) + def cli = parse(args) + setOptionsFromAnnotations(cli, optionInstance.getClass(), optionInstance, true) + optionInstance + } + + void addOptionsFromAnnotations(Class optionClass, boolean namesAreSetters) { + optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method m -> + Annotation annotation = m.getAnnotation(Option) + def typedOption = processAddAnnotation(annotation, m, namesAreSetters) + options.addOption(typedOption.cliOption) + } + + def optionFields = optionClass.declaredFields.findAll { it.getAnnotation(Option) } + if (optionClass.isInterface() && !optionFields.isEmpty()) { + throw new CliBuilderException("@Option only allowed on methods in interface " + optionClass.simpleName) + } + optionFields.each { Field f -> + Annotation annotation = f.getAnnotation(Option) + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + def typedOption = processAddAnnotation(annotation, m, true) + options.addOption(typedOption.cliOption) + } + } + + private TypedOption processAddAnnotation(Option annotation, Method m, boolean namesAreSetters) { + String shortName = annotation.shortName() + String description = annotation.description() + String defaultValue = annotation.defaultValue() + char valueSeparator = 0 + if (annotation.valueSeparator()) valueSeparator = annotation.valueSeparator() as char + boolean optionalArg = annotation.optionalArg() + Integer numberOfArguments = annotation.numberOfArguments() + String numberOfArgumentsString = annotation.numberOfArgumentsString() + Class convert = annotation.convert() + if (convert == Undefined.CLASS) { + convert = null + } + Map names = calculateNames(annotation.longName(), shortName, m, namesAreSetters) + def builder = names.short ? CliOption.builder(names.short) : CliOption.builder() + if (names.long) { + builder.longOpt(names.long) + } + if (numberOfArguments != 1) { + if (numberOfArgumentsString) { + throw new CliBuilderException("You can't specify both 'numberOfArguments' and 'numberOfArgumentsString'") + } + } + def details = [:] + Class type = namesAreSetters ? (m.parameterTypes.size() > 0 ? m.parameterTypes[0] : null) : m.returnType + if (optionalArg && (!type || !type.isArray())) { + throw new CliBuilderException("Attempted to set optional argument for non array type") + } + def isFlag = type.simpleName.toLowerCase() == 'boolean' + if (numberOfArgumentsString) { + details.args = numberOfArgumentsString + details = adjustDetails(details) + if (details.optionalArg) optionalArg = true + } else { + details.args = isFlag ? 0 : numberOfArguments + } + if (details?.args == 0 && !(isFlag || type.name == 'java.lang.Object')) { + throw new CliBuilderException("Flag '${names.long ?: names.short}' must be Boolean or Object") + } + if (description) builder.desc(description) + if (valueSeparator) builder.valueSeparator(valueSeparator) + if (type) { + if (isFlag && details.args == 1) { + // special flag: treat like normal not boolean expecting explicit 'true' or 'false' param + isFlag = false + } + if (!isFlag) { + builder.hasArg(true) + if (details.containsKey('args')) builder.numberOfArgs(details.args) + } + if (type.isArray()) { + builder.optionalArg(optionalArg) + } + } + def typedOption = create(builder.build(), convert ? null : type, defaultValue, convert) + typedOption + } + + private TypedOption create(CliOption o, Class theType, defaultValue, convert) { + Map<String, Object> result = new TypedOption<Object>() + o.with { + if (opt != null) result.put("opt", opt) + result.put("longOpt", longOpt) + result.put("cliOption", o) + if (defaultValue) { + result.put("defaultValue", defaultValue) + } + if (convert) { + if (theType) { + throw new CliBuilderException("You can't specify 'type' when using 'convert'") + } + result.put("convert", convert) + result.put("type", convert instanceof Class ? convert : convert.getClass()) + } else { + result.put("type", theType) + } + } + savedTypeOptions[o.longOpt ?: o.opt] = result + result + } + + def setOptionsFromAnnotations(def cli, Class optionClass, Object t, boolean namesAreSetters) { + optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method m -> + Annotation annotation = m.getAnnotation(Option) + Map names = calculateNames(annotation.longName(), annotation.shortName(), m, namesAreSetters) + processSetAnnotation(m, t, names.long ?: names.short, cli, namesAreSetters) + } + optionClass.declaredFields.findAll { it.getAnnotation(Option) }.each { Field f -> + Annotation annotation = f.getAnnotation(Option) + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + Map names = calculateNames(annotation.longName(), annotation.shortName(), m, true) + processSetAnnotation(m, t, names.long ?: names.short, cli, true) + } + def remaining = cli.arguments() + optionClass.methods.findAll{ it.getAnnotation(Unparsed) }.each { Method m -> + processSetRemaining(m, remaining, t, cli, namesAreSetters) + } + optionClass.declaredFields.findAll{ it.getAnnotation(Unparsed) }.each { Field f -> + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + processSetRemaining(m, remaining, t, cli, namesAreSetters) + } + } + + private void processSetRemaining(Method m, remaining, Object t, cli, boolean namesAreSetters) { + def resultType = namesAreSetters ? m.parameterTypes[0] : m.returnType + def isTyped = resultType?.isArray() + def result + def type = null + if (isTyped) { + type = resultType.componentType + result = remaining.collect{ cli.getValue(type, it, null) } + } else { + result = remaining.toList() + } + if (namesAreSetters) { + m.invoke(t, isTyped ? [result.toArray(Array.newInstance(type, result.size()))] as Object[] : result) + } else { + Map names = calculateNames("", "", m, namesAreSetters) + t.put(names.long, { -> result }) + } + } + + private void processSetAnnotation(Method m, Object t, String name, cli, boolean namesAreSetters) { + def conv = savedTypeOptions[name]?.convert + if (conv && conv instanceof Class) { + savedTypeOptions[name].convert = conv.newInstance(t, t) + } + boolean hasArg = savedTypeOptions[name]?.cliOption?.numberOfArgs == 1 + boolean noArg = savedTypeOptions[name]?.cliOption?.numberOfArgs == 0 + if (namesAreSetters) { + def isBoolArg = m.parameterTypes.size() > 0 && m.parameterTypes[0].simpleName.toLowerCase() == 'boolean' + boolean isFlag = (isBoolArg && !hasArg) || noArg + if (cli.hasOption(name) || isFlag || cli.defaultValue(name)) { + m.invoke(t, [isFlag ? cli.hasOption(name) : + cli.hasOption(name) ? optionValue(cli, name) : cli.defaultValue(name)] as Object[]) + } + } else { + def isBoolRetType = m.returnType.simpleName.toLowerCase() == 'boolean' + boolean isFlag = (isBoolRetType && !hasArg) || noArg + t.put(m.getName(), cli.hasOption(name) ? + { -> isFlag ? true : optionValue(cli, name) } : + { -> isFlag ? false : cli.defaultValue(name) }) + } + } + + private optionValue(cli, String name) { + if (savedTypeOptions.containsKey(name)) { + return cli.getOptionValue(savedTypeOptions[name]) + } + cli[name] + } + + private Map calculateNames(String longName, String shortName, Method m, boolean namesAreSetters) { + boolean useShort = longName == '_' + if (longName == '_') longName = "" + def result = longName + if (!longName) { + result = m.getName() + if (namesAreSetters && result.startsWith("set")) { + result = MetaClassHelper.convertPropertyName(result.substring(3)) + } + } + [long: useShort ? "" : result, short: (useShort && !shortName) ? result : shortName] + } + + // implementation details ------------------------------------- + + /** + * Internal method: How to create an option from the specification. + */ + CliOption option(shortname, Map details, info) { + CliOption option + if (shortname == '_') { + option = CliOption.builder().desc(info).longOpt(details.longOpt).build() + details.remove('longOpt') + } else { + option = new CliOption(shortname, info) + } + adjustDetails(details).each { key, value -> + option[key] = value + } + return option + } + + static Map adjustDetails(Map m) { + m.collectMany { k, v -> + if (k == 'args' && v == '+') { + [[args: org.apache.commons.cli.Option.UNLIMITED_VALUES]] + } else if (k == 'args' && v == '*') { + [[args: org.apache.commons.cli.Option.UNLIMITED_VALUES, + optionalArg: true]] + } else if (k == 'args' && v instanceof String) { + [[args: Integer.parseInt(v)]] + } else { + [[(k): v]] + } + }.sum() + } + + static expandArgumentFiles(args) throws IOException { + def result = [] + for (arg in args) { + if (arg && arg != '@' && arg[0] == '@') { + arg = arg.substring(1) + if (arg[0] != '@') { + expandArgumentFile(arg, result) + continue + } + } + result << arg + } + return result + } + + private static expandArgumentFile(name, args) throws IOException { + def charAsInt = { String s -> s.toCharacter() as int } + new File(name).withReader { r -> + new StreamTokenizer(r).with { + resetSyntax() + wordChars(charAsInt(' '), 255) + whitespaceChars(0, charAsInt(' ')) + commentChar(charAsInt('#')) + quoteChar(charAsInt('"')) + quoteChar(charAsInt('\'')) + while (nextToken() != StreamTokenizer.TT_EOF) { + args << sval + } + } + } + } + +} + +class OptionAccessor { + CommandLine commandLine + Map<String, TypedOption> savedTypeOptions + + OptionAccessor(CommandLine commandLine) { + this.commandLine = commandLine + } + + boolean hasOption(TypedOption typedOption) { + commandLine.hasOption(typedOption.longOpt ?: typedOption.opt) + } + + public <T> T defaultValue(String name) { + Class<T> type = savedTypeOptions[name]?.type + String value = savedTypeOptions[name]?.defaultValue() ? savedTypeOptions[name].defaultValue() : null + return (T) value ? getTypedValue(type, name, value) : null + } + + public <T> T getOptionValue(TypedOption<T> typedOption) { + getOptionValue(typedOption, null) + } + + public <T> T getOptionValue(TypedOption<T> typedOption, T defaultValue) { + String optionName = (String) typedOption.longOpt ?: typedOption.opt + if (commandLine.hasOption(optionName)) { + if (typedOption.containsKey('type') && typedOption.type.isArray()) { + def compType = typedOption.type.componentType + return (T) getTypedValuesFromName(optionName, compType) + } + return getTypedValueFromName(optionName) + } + return defaultValue + } + + private <T> T[] getTypedValuesFromName(String optionName, Class<T> compType) { + CliOption option = commandLine.options.find{ it.longOpt == optionName } + T[] result = null + if (option) { + int count = 0 + def optionValues = commandLine.getOptionValues(optionName) + for (String optionValue : optionValues) { + if (result == null) { + result = (T[]) Array.newInstance(compType, optionValues.length) + } + result[count++] = (T) getTypedValue(compType, optionName, optionValue) + } + } + if (result == null) { + result = (T[]) Array.newInstance(compType, 0) + } + return result + } + + public <T> T getAt(TypedOption<T> typedOption) { + getAt(typedOption, null) + } + + public <T> T getAt(TypedOption<T> typedOption, T defaultValue) { + String optionName = (String) typedOption.longOpt ?: typedOption.opt + if (savedTypeOptions.containsKey(optionName)) { + return getTypedValueFromName(optionName) + } + return defaultValue + } + + private <T> T getTypedValueFromName(String optionName) { + Class type = savedTypeOptions[optionName].type + String optionValue = commandLine.getOptionValue(optionName) + return (T) getTypedValue(type, optionName, optionValue) + } + + private <T> T getTypedValue(Class<T> type, String optionName, String optionValue) { + if (savedTypeOptions[optionName]?.cliOption?.numberOfArgs == 0) { + return (T) commandLine.hasOption(optionName) + } + def convert = savedTypeOptions[optionName]?.convert + return getValue(type, optionValue, convert) + } + + private <T> T getValue(Class<T> type, String optionValue, Closure convert) { + if (!type) { + return (T) optionValue + } + if (Closure.isAssignableFrom(type) && convert) { + return (T) convert(optionValue) + } + if (type?.simpleName?.toLowerCase() == 'boolean') { + return (T) Boolean.parseBoolean(optionValue) + } + StringGroovyMethods.asType(optionValue, (Class<T>) type) + } + + def invokeMethod(String name, Object args) { + return InvokerHelper.getMetaClass(commandLine).invokeMethod(commandLine, name, args) + } + + def getProperty(String name) { + if (!savedTypeOptions.containsKey(name)) { + def alt = savedTypeOptions.find{ it.value.opt == name } + if (alt) name = alt.key + } + def methodname = 'getOptionValue' + Class type = savedTypeOptions[name]?.type + def foundArray = type?.isArray() + if (name.size() > 1 && name.endsWith('s')) { + def singularName = name[0..-2] + if (commandLine.hasOption(singularName) || foundArray) { + name = singularName + methodname += 's' + type = savedTypeOptions[name]?.type + } + } + if (type?.isArray()) { + methodname = 'getOptionValues' + } + if (name.size() == 1) name = name as char + def result = InvokerHelper.getMetaClass(commandLine).invokeMethod(commandLine, methodname, name) + if (result != null) { + if (result instanceof String[]) { + result = result.collect{ type ? getTypedValue(type.isArray() ? type.componentType : type, name, it) : it } + } else { + if (type) result = getTypedValue(type, name, result) + } + } else if (type?.simpleName != 'boolean' && savedTypeOptions[name]?.defaultValue) { + result = getTypedValue(type, name, savedTypeOptions[name].defaultValue) + } else { + result = commandLine.hasOption(name) + } + return result + } + + List<String> arguments() { + commandLine.args.toList() + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/ClosureComparator.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/util/ClosureComparator.java b/src/main/groovy/groovy/util/ClosureComparator.java new file mode 100644 index 0000000..dc70ea6 --- /dev/null +++ b/src/main/groovy/groovy/util/ClosureComparator.java @@ -0,0 +1,45 @@ +/* + * 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 org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * A Comparator which uses a closure to compare 2 values being equal + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class ClosureComparator<T> implements Comparator<T>, Serializable { + + private static final long serialVersionUID = -4593521535656429522L; + Closure closure; + + public ClosureComparator(Closure closure) { + this.closure = closure; + } + + public int compare(T object1, T object2) { + Object value = closure.call(object1, object2); + return DefaultTypeTransformation.intUnbox(value); + } +}
