http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/BindableASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/BindableASTTransformation.java b/src/main/groovy/groovy/beans/BindableASTTransformation.java new file mode 100644 index 0000000..e1bf2e5 --- /dev/null +++ b/src/main/groovy/groovy/beans/BindableASTTransformation.java @@ -0,0 +1,428 @@ +/* + * 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.beans; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.ast.tools.PropertyNodeUtils; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.SimpleMessage; +import org.codehaus.groovy.control.messages.SyntaxErrorMessage; +import org.codehaus.groovy.runtime.MetaClassHelper; +import org.codehaus.groovy.syntax.SyntaxException; +import org.codehaus.groovy.transform.ASTTransformation; +import org.codehaus.groovy.transform.GroovyASTTransformation; +import org.objectweb.asm.Opcodes; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +import static org.codehaus.groovy.ast.tools.GeneralUtils.args; +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.declS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.fieldX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; + +/** + * Handles generation of code for the {@code @Bindable} annotation when {@code @Vetoable} + * is not present. + * <p> + * Generally, it adds (if needed) a PropertyChangeSupport field and + * the needed add/removePropertyChangeListener methods to support the + * listeners. + * <p> + * It also generates the setter and wires the setter through the + * PropertyChangeSupport. + * <p> + * If a {@link Vetoable} annotation is detected it does nothing and + * lets the {@link VetoableASTTransformation} handle all the changes. + * + * @author Danno Ferrin (shemnon) + * @author Chris Reeves + */ +@GroovyASTTransformation(phase= CompilePhase.CANONICALIZATION) +public class BindableASTTransformation implements ASTTransformation, Opcodes { + + protected static final ClassNode boundClassNode = ClassHelper.make(Bindable.class); + + /** + * Convenience method to see if an annotated node is {@code @Bindable}. + * + * @param node the node to check + * @return true if the node is bindable + */ + public static boolean hasBindableAnnotation(AnnotatedNode node) { + for (AnnotationNode annotation : node.getAnnotations()) { + if (boundClassNode.equals(annotation.getClassNode())) { + return true; + } + } + return false; + } + + /** + * Handles the bulk of the processing, mostly delegating to other methods. + * + * @param nodes the ast nodes + * @param source the source unit for the nodes + */ + public void visit(ASTNode[] nodes, SourceUnit source) { + if (!(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { + throw new RuntimeException("Internal error: wrong types: $node.class / $parent.class"); + } + AnnotationNode node = (AnnotationNode) nodes[0]; + AnnotatedNode parent = (AnnotatedNode) nodes[1]; + + if (VetoableASTTransformation.hasVetoableAnnotation(parent)) { + // VetoableASTTransformation will handle both @Bindable and @Vetoable + return; + } + + ClassNode declaringClass = parent.getDeclaringClass(); + if (parent instanceof FieldNode) { + if ((((FieldNode) parent).getModifiers() & Opcodes.ACC_FINAL) != 0) { + source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( + new SyntaxException("@groovy.beans.Bindable cannot annotate a final property.", + node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), + source)); + } + + if (VetoableASTTransformation.hasVetoableAnnotation(parent.getDeclaringClass())) { + // VetoableASTTransformation will handle both @Bindable and @Vetoable + return; + } + addListenerToProperty(source, node, declaringClass, (FieldNode) parent); + } else if (parent instanceof ClassNode) { + addListenerToClass(source, (ClassNode) parent); + } + } + + private void addListenerToProperty(SourceUnit source, AnnotationNode node, ClassNode declaringClass, FieldNode field) { + String fieldName = field.getName(); + for (PropertyNode propertyNode : declaringClass.getProperties()) { + if (propertyNode.getName().equals(fieldName)) { + if (field.isStatic()) { + //noinspection ThrowableInstanceNeverThrown + source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( + new SyntaxException("@groovy.beans.Bindable cannot annotate a static property.", + node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), + source)); + } else { + if (needsPropertyChangeSupport(declaringClass, source)) { + addPropertyChangeSupport(declaringClass); + } + createListenerSetter(declaringClass, propertyNode); + } + return; + } + } + //noinspection ThrowableInstanceNeverThrown + source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( + new SyntaxException("@groovy.beans.Bindable must be on a property, not a field. Try removing the private, protected, or public modifier.", + node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), + source)); + } + + private void addListenerToClass(SourceUnit source, ClassNode classNode) { + if (needsPropertyChangeSupport(classNode, source)) { + addPropertyChangeSupport(classNode); + } + for (PropertyNode propertyNode : classNode.getProperties()) { + FieldNode field = propertyNode.getField(); + // look to see if per-field handlers will catch this one... + if (hasBindableAnnotation(field) + || ((field.getModifiers() & Opcodes.ACC_FINAL) != 0) + || field.isStatic() + || VetoableASTTransformation.hasVetoableAnnotation(field)) + { + // explicitly labeled properties are already handled, + // don't transform final properties + // don't transform static properties + // VetoableASTTransformation will handle both @Bindable and @Vetoable + continue; + } + createListenerSetter(classNode, propertyNode); + } + } + + /* + * Wrap an existing setter. + */ + private static void wrapSetterMethod(ClassNode classNode, String propertyName) { + String getterName = "get" + MetaClassHelper.capitalize(propertyName); + MethodNode setter = classNode.getSetterMethod("set" + MetaClassHelper.capitalize(propertyName)); + + if (setter != null) { + // Get the existing code block + Statement code = setter.getCode(); + + Expression oldValue = varX("$oldValue"); + Expression newValue = varX("$newValue"); + BlockStatement block = new BlockStatement(); + + // create a local variable to hold the old value from the getter + block.addStatement(declS(oldValue, callThisX(getterName))); + + // call the existing block, which will presumably set the value properly + block.addStatement(code); + + // get the new value to emit in the event + block.addStatement(declS(newValue, callThisX(getterName))); + + // add the firePropertyChange method call + block.addStatement(stmt(callThisX("firePropertyChange", args(constX(propertyName), oldValue, newValue)))); + + // replace the existing code block with our new one + setter.setCode(block); + } + } + + private void createListenerSetter(ClassNode classNode, PropertyNode propertyNode) { + String setterName = "set" + MetaClassHelper.capitalize(propertyNode.getName()); + if (classNode.getMethods(setterName).isEmpty()) { + Statement setterBlock = createBindableStatement(propertyNode, fieldX(propertyNode.getField())); + + // create method void <setter>(<type> fieldName) + createSetterMethod(classNode, propertyNode, setterName, setterBlock); + } else { + wrapSetterMethod(classNode, propertyNode.getName()); + } + } + + /** + * Creates a statement body similar to: + * <code>this.firePropertyChange("field", field, field = value)</code> + * + * @param propertyNode the field node for the property + * @param fieldExpression a field expression for setting the property value + * @return the created statement + */ + protected Statement createBindableStatement(PropertyNode propertyNode, Expression fieldExpression) { + // create statementBody + return stmt(callThisX("firePropertyChange", args(constX(propertyNode.getName()), fieldExpression, assignX(fieldExpression, varX("value"))))); + } + + /** + * Creates a setter method with the given body. + * + * @param declaringClass the class to which we will add the setter + * @param propertyNode the field to back the setter + * @param setterName the name of the setter + * @param setterBlock the statement representing the setter block + */ + protected void createSetterMethod(ClassNode declaringClass, PropertyNode propertyNode, String setterName, Statement setterBlock) { + MethodNode setter = new MethodNode( + setterName, + PropertyNodeUtils.adjustPropertyModifiersForMethod(propertyNode), + ClassHelper.VOID_TYPE, + params(param(propertyNode.getType(), "value")), + ClassNode.EMPTY_ARRAY, + setterBlock); + setter.setSynthetic(true); + // add it to the class + declaringClass.addMethod(setter); + } + + /** + * Snoops through the declaring class and all parents looking for methods + * <code>void addPropertyChangeListener(PropertyChangeListener)</code>, + * <code>void removePropertyChangeListener(PropertyChangeListener)</code>, and + * <code>void firePropertyChange(String, Object, Object)</code>. If any are defined all + * must be defined or a compilation error results. + * + * @param declaringClass the class to search + * @param sourceUnit the source unit, for error reporting. {@code @NotNull}. + * @return true if property change support should be added + */ + protected boolean needsPropertyChangeSupport(ClassNode declaringClass, SourceUnit sourceUnit) { + boolean foundAdd = false, foundRemove = false, foundFire = false; + ClassNode consideredClass = declaringClass; + while (consideredClass!= null) { + for (MethodNode method : consideredClass.getMethods()) { + // just check length, MOP will match it up + foundAdd = foundAdd || method.getName().equals("addPropertyChangeListener") && method.getParameters().length == 1; + foundRemove = foundRemove || method.getName().equals("removePropertyChangeListener") && method.getParameters().length == 1; + foundFire = foundFire || method.getName().equals("firePropertyChange") && method.getParameters().length == 3; + if (foundAdd && foundRemove && foundFire) { + return false; + } + } + consideredClass = consideredClass.getSuperClass(); + } + // check if a super class has @Bindable annotations + consideredClass = declaringClass.getSuperClass(); + while (consideredClass!=null) { + if (hasBindableAnnotation(consideredClass)) return false; + for (FieldNode field : consideredClass.getFields()) { + if (hasBindableAnnotation(field)) return false; + } + consideredClass = consideredClass.getSuperClass(); + } + if (foundAdd || foundRemove || foundFire) { + sourceUnit.getErrorCollector().addErrorAndContinue( + new SimpleMessage("@Bindable cannot be processed on " + + declaringClass.getName() + + " because some but not all of addPropertyChangeListener, removePropertyChange, and firePropertyChange were declared in the current or super classes.", + sourceUnit) + ); + return false; + } + return true; + } + + /** + * Adds the necessary field and methods to support property change support. + * <p> + * Adds a new field: + * <pre> + * <code>protected final java.beans.PropertyChangeSupport this$PropertyChangeSupport = new java.beans.PropertyChangeSupport(this)</code>" + * </pre> + * <p> + * Also adds support methods: + * <pre> + * <code>public void addPropertyChangeListener(java.beans.PropertyChangeListener)</code> + * <code>public void addPropertyChangeListener(String, java.beans.PropertyChangeListener)</code> + * <code>public void removePropertyChangeListener(java.beans.PropertyChangeListener)</code> + * <code>public void removePropertyChangeListener(String, java.beans.PropertyChangeListener)</code> + * <code>public java.beans.PropertyChangeListener[] getPropertyChangeListeners()</code> + * </pre> + * + * @param declaringClass the class to which we add the support field and methods + */ + protected void addPropertyChangeSupport(ClassNode declaringClass) { + ClassNode pcsClassNode = ClassHelper.make(PropertyChangeSupport.class); + ClassNode pclClassNode = ClassHelper.make(PropertyChangeListener.class); + //String pcsFieldName = "this$propertyChangeSupport"; + + // add field: + // protected final PropertyChangeSupport this$propertyChangeSupport = new java.beans.PropertyChangeSupport(this) + FieldNode pcsField = declaringClass.addField( + "this$propertyChangeSupport", + ACC_FINAL | ACC_PRIVATE | ACC_SYNTHETIC, + pcsClassNode, + ctorX(pcsClassNode, args(varX("this")))); + + // add method: + // void addPropertyChangeListener(listener) { + // this$propertyChangeSupport.addPropertyChangeListener(listener) + // } + declaringClass.addMethod( + new MethodNode( + "addPropertyChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(pclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(pcsField), "addPropertyChangeListener", args(varX("listener", pclClassNode)))))); + + // add method: + // void addPropertyChangeListener(name, listener) { + // this$propertyChangeSupport.addPropertyChangeListener(name, listener) + // } + declaringClass.addMethod( + new MethodNode( + "addPropertyChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(ClassHelper.STRING_TYPE, "name"), param(pclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(pcsField), "addPropertyChangeListener", args(varX("name", ClassHelper.STRING_TYPE), varX("listener", pclClassNode)))))); + + // add method: + // boolean removePropertyChangeListener(listener) { + // return this$propertyChangeSupport.removePropertyChangeListener(listener); + // } + declaringClass.addMethod( + new MethodNode( + "removePropertyChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(pclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(pcsField), "removePropertyChangeListener", args(varX("listener", pclClassNode)))))); + + // add method: void removePropertyChangeListener(name, listener) + declaringClass.addMethod( + new MethodNode( + "removePropertyChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(ClassHelper.STRING_TYPE, "name"), param(pclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(pcsField), "removePropertyChangeListener", args(varX("name", ClassHelper.STRING_TYPE), varX("listener", pclClassNode)))))); + + // add method: + // void firePropertyChange(String name, Object oldValue, Object newValue) { + // this$propertyChangeSupport.firePropertyChange(name, oldValue, newValue) + // } + declaringClass.addMethod( + new MethodNode( + "firePropertyChange", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(ClassHelper.STRING_TYPE, "name"), param(ClassHelper.OBJECT_TYPE, "oldValue"), param(ClassHelper.OBJECT_TYPE, "newValue")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(pcsField), "firePropertyChange", args(varX("name", ClassHelper.STRING_TYPE), varX("oldValue"), varX("newValue")))))); + + // add method: + // PropertyChangeListener[] getPropertyChangeListeners() { + // return this$propertyChangeSupport.getPropertyChangeListeners + // } + declaringClass.addMethod( + new MethodNode( + "getPropertyChangeListeners", + ACC_PUBLIC, + pclClassNode.makeArray(), + Parameter.EMPTY_ARRAY, + ClassNode.EMPTY_ARRAY, + returnS(callX(fieldX(pcsField), "getPropertyChangeListeners")))); + + // add method: + // PropertyChangeListener[] getPropertyChangeListeners(String name) { + // return this$propertyChangeSupport.getPropertyChangeListeners(name) + // } + declaringClass.addMethod( + new MethodNode( + "getPropertyChangeListeners", + ACC_PUBLIC, + pclClassNode.makeArray(), + params(param(ClassHelper.STRING_TYPE, "name")), + ClassNode.EMPTY_ARRAY, + returnS(callX(fieldX(pcsField), "getPropertyChangeListeners", args(varX("name", ClassHelper.STRING_TYPE)))))); + } +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/DefaultPropertyAccessor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/DefaultPropertyAccessor.java b/src/main/groovy/groovy/beans/DefaultPropertyAccessor.java new file mode 100644 index 0000000..47dae41 --- /dev/null +++ b/src/main/groovy/groovy/beans/DefaultPropertyAccessor.java @@ -0,0 +1,34 @@ +/* + * 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.beans; + +/** + * @author Andres Almiray + */ +public class DefaultPropertyAccessor implements PropertyAccessor { + public static final PropertyAccessor INSTANCE = new DefaultPropertyAccessor(); + + public Object read(Object owner, String propertyName) { + return DefaultPropertyReader.INSTANCE.read(owner, propertyName); + } + + public void write(Object owner, String propertyName, Object value) { + DefaultPropertyWriter.INSTANCE.write(owner, propertyName, value); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/DefaultPropertyReader.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/DefaultPropertyReader.java b/src/main/groovy/groovy/beans/DefaultPropertyReader.java new file mode 100644 index 0000000..a03b9a3 --- /dev/null +++ b/src/main/groovy/groovy/beans/DefaultPropertyReader.java @@ -0,0 +1,32 @@ +/* + * 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.beans; + +import org.codehaus.groovy.runtime.InvokerHelper; + +/** + * @author Andres Almiray + */ +public class DefaultPropertyReader implements PropertyReader { + public static final PropertyReader INSTANCE = new DefaultPropertyReader(); + + public Object read(Object owner, String propertyName) { + return InvokerHelper.getPropertySafe(owner, propertyName); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/DefaultPropertyWriter.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/DefaultPropertyWriter.java b/src/main/groovy/groovy/beans/DefaultPropertyWriter.java new file mode 100644 index 0000000..12ac7db --- /dev/null +++ b/src/main/groovy/groovy/beans/DefaultPropertyWriter.java @@ -0,0 +1,32 @@ +/* + * 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.beans; + +import org.codehaus.groovy.runtime.InvokerHelper; + +/** + * @author Andres Almiray + */ +public class DefaultPropertyWriter implements PropertyWriter { + public static final PropertyWriter INSTANCE = new DefaultPropertyWriter(); + + public void write(Object owner, String propertyName, Object value) { + InvokerHelper.setProperty(owner, propertyName, value); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/ListenerList.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/ListenerList.groovy b/src/main/groovy/groovy/beans/ListenerList.groovy new file mode 100644 index 0000000..b8119f1 --- /dev/null +++ b/src/main/groovy/groovy/beans/ListenerList.groovy @@ -0,0 +1,131 @@ +/* + * 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.beans + +import org.codehaus.groovy.transform.GroovyASTTransformationClass + +import java.lang.annotation.Documented +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * This annotation adds Java-style listener support to a class based on an annotated Collection-property. + * <p> + * For any given Collection property, several methods will be written into the enclosing class during the compile phase. These + * changes are visible from Java or other languages. The List is intended to hold listeners of some sort, and the methods + * addListener, removeListener, and getListeners are all added to the class. The actual methods names depend on the generic + * type of the collection. + * <p> + * Given the following example:<br> + * <pre> + * class MyClass { + * @groovy.beans.ListenerList + * List<java.awt.event.ActionListener> listeners + * } + * </pre> + * The following code is generated: + * <pre> + * public class MyClass extends java.lang.Object { + * @groovy.beans.ListenerList + * private java.util.List<java.awt.event.ActionListener> listeners + * + * public void addActionListener(java.awt.event.ActionListener listener) { + * if ( listener == null) { + * return null + * } + * if ( listeners == null) { + * listeners = [] + * } + * listeners.add(listener) + * } + * + * public void removeActionListener(java.awt.event.ActionListener listener) { + * if ( listener == null) { + * return null + * } + * if ( listeners == null) { + * listeners = [] + * } + * listeners.remove(listener) + * } + * + * public java.awt.event.ActionListener[] getActionListeners() { + * java.lang.Object __result = [] + * if ( listeners != null) { + * __result.addAll(listeners) + * } + * return (( __result ) as java.awt.event.ActionListener[]) + * } + * + * public void fireActionPerformed(java.awt.event.ActionEvent param0) { + * if ( listeners != null) { + * def __list = new java.util.ArrayList(listeners) + * for (java.lang.Object listener : __list ) { + * listener.actionPerformed(param0) + * } + * } + * } + * } + * </pre> + * A fire method is created for each public method in the target class. In this case, ActionListener only has one + * method. For a four method interface, four fire methods would be created. + * <p> + * The annotation can take the following parameters: + * <pre> + * name = a suffix for creating the add, remove, and get methods. + * Default: Name of the listener type + * In the above example, if name is set to MyListener, then the class will have an addMyListener, + * removeMyListener, and getMyListeners methods. + * + * synchronize = Whether or not the methods created should be synchronized at the method level. + * Default: false + * </pre> + * <p> + * <strong>Compilation Errors</strong> - Using this annotation incorrectly results in compilation errors rather + * than runtime errors. A list of potential problems includes: + * <ul> + * <li>This annotation can only be applied to a field of type Collection</li> + * <li>The annotated Collection field must have a generic type</li> + * <li>The annotated Collection field must not have a generic wildcard declared</li> + * <li>The generated methods must not already exist</li> + * </ul> + * + * @see ListenerListASTTransformation + * @author Alexander Klein + * @author Hamlet D'Arcy + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +@GroovyASTTransformationClass('groovy.beans.ListenerListASTTransformation') +@interface ListenerList { + /** + * A suffix for creating the add, remove, and get methods + * defaulting to the name of the listener type, e.g. if name is set to MyListener, + * then the class will have addMyListener, removeMyListener, and getMyListeners methods. + */ + String name() default "" + + /** + * Whether or not the methods created should be synchronized at the method level. + */ + boolean synchronize() default false +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/ListenerListASTTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/ListenerListASTTransformation.groovy b/src/main/groovy/groovy/beans/ListenerListASTTransformation.groovy new file mode 100644 index 0000000..1d7fbf7 --- /dev/null +++ b/src/main/groovy/groovy/beans/ListenerListASTTransformation.groovy @@ -0,0 +1,384 @@ +/* + * 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.beans + +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotatedNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.GenericsType +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.VariableScope +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.BinaryExpression +import org.codehaus.groovy.ast.expr.BooleanExpression +import org.codehaus.groovy.ast.expr.CastExpression +import org.codehaus.groovy.ast.expr.ConstantExpression +import org.codehaus.groovy.ast.expr.ConstructorCallExpression +import org.codehaus.groovy.ast.expr.DeclarationExpression +import org.codehaus.groovy.ast.expr.ListExpression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.EmptyStatement +import org.codehaus.groovy.ast.stmt.ExpressionStatement +import org.codehaus.groovy.ast.stmt.ForStatement +import org.codehaus.groovy.ast.stmt.IfStatement +import org.codehaus.groovy.ast.stmt.ReturnStatement +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.control.messages.SyntaxErrorMessage +import org.codehaus.groovy.syntax.SyntaxException +import org.codehaus.groovy.syntax.Token +import org.codehaus.groovy.syntax.Types +import org.codehaus.groovy.transform.ASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.objectweb.asm.Opcodes + +/** + * Handles generation of code for the {@code @ListenerList} annotation. + * <p> + * Generally, it adds the needed add<Listener>, remove<Listener> and + * get<Listener>s methods to support the Java Beans API. + * <p> + * Additionally it adds corresponding fire<Event> methods. + * <p> + * + * @author Alexander Klein + * @author Hamlet D'Arcy + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +class ListenerListASTTransformation implements ASTTransformation, Opcodes { + private static final Class MY_CLASS = groovy.beans.ListenerList.class + private static final ClassNode COLLECTION_TYPE = ClassHelper.make(Collection) + + public void visit(ASTNode[] nodes, SourceUnit source) { + if (!(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { + throw new RuntimeException("Internal error: wrong types: ${node.class} / ${parent.class}") + } + AnnotationNode node = nodes[0] + FieldNode field = nodes[1] + ClassNode declaringClass = nodes[1].declaringClass + ClassNode parentClass = field.type + + boolean isCollection = parentClass.isDerivedFrom(COLLECTION_TYPE) || parentClass.implementsInterface(COLLECTION_TYPE) + + if (!isCollection) { + addError(node, source, '@' + MY_CLASS.name + ' can only annotate collection properties.') + return + } + + def types = field.type.genericsTypes + if (!types) { + addError(node, source, '@' + MY_CLASS.name + ' fields must have a generic type.') + return + } + + if (types[0].wildcard) { + addError(node, source, '@' + MY_CLASS.name + ' fields with generic wildcards not yet supported.') + return + } + + def listener = types[0].type + + if (!field.initialValueExpression) { + field.initialValueExpression = new ListExpression() + } + + def name = node.getMember('name')?.value ?: listener.nameWithoutPackage + + def fireList = listener.methods.findAll { MethodNode m -> + m.isPublic() && !m.isSynthetic() && !m.isStatic() + } + + def synchronize = node.getMember('synchronize')?.value ?: false + addAddListener(source, node, declaringClass, field, listener, name, synchronize) + addRemoveListener(source, node, declaringClass, field, listener, name, synchronize) + addGetListeners(source, node, declaringClass, field, listener, name, synchronize) + + fireList.each { MethodNode method -> + addFireMethods(source, node, declaringClass, field, types, synchronize, method) + } + } + + private static def addError(AnnotationNode node, SourceUnit source, String message) { + source.errorCollector.addError( + new SyntaxErrorMessage(new SyntaxException( + message, + node.lineNumber, + node.columnNumber), + source)) + } + + /** + * Adds the add<Listener> method like: + * <pre> + * synchronized void add${name.capitalize}(${listener.name} listener) { + * if (listener == null) + * return + * if (${field.name} == null) + * ${field.name} = [] + * ${field.name}.add(listener) + * } + * </pre> + */ + void addAddListener(SourceUnit source, AnnotationNode node, ClassNode declaringClass, FieldNode field, ClassNode listener, String name, synchronize) { + + def methodModifiers = synchronize ? ACC_PUBLIC | ACC_SYNCHRONIZED : ACC_PUBLIC + def methodReturnType = ClassHelper.make(Void.TYPE) + def methodName = "add${name.capitalize()}" + def cn = ClassHelper.makeWithoutCaching(listener.name) + cn.redirect = listener + def methodParameter = [new Parameter(cn,'listener')] as Parameter[] + + if (declaringClass.hasMethod(methodName, methodParameter)) { + addError node, source, "Conflict using @${MY_CLASS.name}. Class $declaringClass.name already has method $methodName" + return + } + + BlockStatement block = new BlockStatement() + block.addStatements([ + new IfStatement( + new BooleanExpression( + new BinaryExpression( + new VariableExpression('listener'), + Token.newSymbol(Types.COMPARE_EQUAL, 0, 0), + ConstantExpression.NULL + ) + ), + new ReturnStatement(ConstantExpression.NULL), + EmptyStatement.INSTANCE + ), + new IfStatement( + new BooleanExpression( + new BinaryExpression( + new VariableExpression(field.name), + Token.newSymbol(Types.COMPARE_EQUAL, 0, 0), + ConstantExpression.NULL + ) + ), + new ExpressionStatement( + new BinaryExpression( + new VariableExpression(field.name), + Token.newSymbol(Types.EQUAL, 0, 0), + new ListExpression() + ) + ), + EmptyStatement.INSTANCE + ), + new ExpressionStatement( + new MethodCallExpression(new VariableExpression(field.name), new ConstantExpression('add'), new ArgumentListExpression(new VariableExpression('listener'))) + ) + ]) + declaringClass.addMethod(new MethodNode(methodName, methodModifiers, methodReturnType, methodParameter, [] as ClassNode[], block)) + } + + /** + * Adds the remove<Listener> method like: + * <pre> + * synchronized void remove${name.capitalize}(${listener.name} listener) { + * if (listener == null) + * return + * if (${field.name} == null) + * ${field.name} = [] + * ${field.name}.remove(listener) + * } + * </pre> + */ + void addRemoveListener(SourceUnit source, AnnotationNode node, ClassNode declaringClass, FieldNode field, ClassNode listener, String name, synchronize) { + def methodModifiers = synchronize ? ACC_PUBLIC | ACC_SYNCHRONIZED : ACC_PUBLIC + def methodReturnType = ClassHelper.make(Void.TYPE) + def methodName = "remove${name.capitalize()}" + def cn = ClassHelper.makeWithoutCaching(listener.name) + cn.redirect = listener + def methodParameter = [new Parameter(cn,'listener')] as Parameter[] + + if (declaringClass.hasMethod(methodName, methodParameter)) { + addError node, source, "Conflict using @${MY_CLASS.name}. Class $declaringClass.name already has method $methodName" + return + } + + BlockStatement block = new BlockStatement() + block.addStatements([ + new IfStatement( + new BooleanExpression( + new BinaryExpression( + new VariableExpression('listener'), + Token.newSymbol(Types.COMPARE_EQUAL, 0, 0), + ConstantExpression.NULL + ) + ), + new ReturnStatement(ConstantExpression.NULL), + EmptyStatement.INSTANCE + ), + new IfStatement( + new BooleanExpression( + new BinaryExpression( + new VariableExpression(field.name), + Token.newSymbol(Types.COMPARE_EQUAL, 0, 0), + ConstantExpression.NULL + ) + ), + new ExpressionStatement( + new BinaryExpression( + new VariableExpression(field.name), + Token.newSymbol(Types.EQUAL, 0, 0), + new ListExpression() + ) + ), + EmptyStatement.INSTANCE + ), + new ExpressionStatement( + new MethodCallExpression(new VariableExpression(field.name), new ConstantExpression('remove'), new ArgumentListExpression(new VariableExpression("listener"))) + ) + ]) + declaringClass.addMethod(new MethodNode(methodName, methodModifiers, methodReturnType, methodParameter, [] as ClassNode[], block)) + } + + /** + * Adds the get<Listener>s method like: + * <pre> + * synchronized ${name.capitalize}[] get${name.capitalize}s() { + * def __result = [] + * if (${field.name} != null) + * __result.addAll(${field.name}) + * return __result as ${name.capitalize}[] + * } + * </pre> + */ + void addGetListeners(SourceUnit source, AnnotationNode node, ClassNode declaringClass, FieldNode field, ClassNode listener, String name, synchronize) { + def methodModifiers = synchronize ? ACC_PUBLIC | ACC_SYNCHRONIZED : ACC_PUBLIC + def methodReturnType = listener.makeArray() + def methodName = "get${name.capitalize()}s" + def methodParameter = [] as Parameter[] + + if (declaringClass.hasMethod(methodName, methodParameter)) { + addError node, source, "Conflict using @${MY_CLASS.name}. Class $declaringClass.name already has method $methodName" + return + } + + BlockStatement block = new BlockStatement() + block.addStatements([ + new ExpressionStatement( + new DeclarationExpression( + new VariableExpression("__result", ClassHelper.DYNAMIC_TYPE), + Token.newSymbol(Types.EQUALS, 0, 0), + new ListExpression() + )), + new IfStatement( + new BooleanExpression( + new BinaryExpression( + new VariableExpression(field.name), + Token.newSymbol(Types.COMPARE_NOT_EQUAL, 0, 0), + ConstantExpression.NULL + ) + ), + new ExpressionStatement( + new MethodCallExpression(new VariableExpression('__result'), new ConstantExpression('addAll'), new ArgumentListExpression(new VariableExpression(field.name))) + ), + EmptyStatement.INSTANCE + ), + new ReturnStatement( + new CastExpression( + methodReturnType, + new VariableExpression('__result') + ) + ) + ]) + declaringClass.addMethod(new MethodNode(methodName, methodModifiers, methodReturnType, methodParameter, [] as ClassNode[], block)) + } + + /** + * Adds the fire<Event> methods like: + * <pre> + * void fire${fireMethod.capitalize()}(${parameterList.join(', ')}) { + * if (${field.name} != null) { + * def __list = new ArrayList(${field.name}) + * __list.each { listener -> + * listener.$eventMethod(${evt}) + * } + * } + * } + * </pre> + */ + void addFireMethods(SourceUnit source, AnnotationNode node, ClassNode declaringClass, FieldNode field, GenericsType[] types, boolean synchronize, MethodNode method) { + + def methodReturnType = ClassHelper.make(Void.TYPE) + def methodName = "fire${method.name.capitalize()}" + def methodModifiers = synchronize ? ACC_PUBLIC | ACC_SYNCHRONIZED : ACC_PUBLIC + + if (declaringClass.hasMethod(methodName, method.parameters)) { + addError node, source, "Conflict using @${MY_CLASS.name}. Class $declaringClass.name already has method $methodName" + return + } + + def args = new ArgumentListExpression(method.parameters) + + BlockStatement block = new BlockStatement() + def listenerListType = ClassHelper.make(ArrayList).plainNodeReference + listenerListType.setGenericsTypes(types) + block.addStatements([ + new IfStatement( + new BooleanExpression( + new BinaryExpression( + new VariableExpression(field.name), + Token.newSymbol(Types.COMPARE_NOT_EQUAL, 0, 0), + ConstantExpression.NULL + ) + ), + new BlockStatement([ + new ExpressionStatement( + new DeclarationExpression( + new VariableExpression('__list', listenerListType), + Token.newSymbol(Types.EQUALS, 0, 0), + new ConstructorCallExpression(listenerListType, new ArgumentListExpression( + new VariableExpression(field.name) + )) + ) + ), + new ForStatement( + new Parameter(ClassHelper.DYNAMIC_TYPE, 'listener'), + new VariableExpression('__list'), + new BlockStatement([ + new ExpressionStatement( + new MethodCallExpression( + new VariableExpression('listener'), + method.name, + args + ) + ) + ], new VariableScope()) + ) + ], new VariableScope()), + EmptyStatement.INSTANCE + ) + ]) + + def params = method.parameters.collect { + def paramType = ClassHelper.getWrapper(it.type) + def cn = paramType.plainNodeReference + cn.setRedirect(paramType) + new Parameter(cn, it.name) + } + declaringClass.addMethod(methodName, methodModifiers, methodReturnType, params as Parameter[], [] as ClassNode[], block) + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/PropertyAccessor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/PropertyAccessor.java b/src/main/groovy/groovy/beans/PropertyAccessor.java new file mode 100644 index 0000000..822ab10 --- /dev/null +++ b/src/main/groovy/groovy/beans/PropertyAccessor.java @@ -0,0 +1,25 @@ +/* + * 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.beans; + +/** + * @author Andres Almiray + */ +public interface PropertyAccessor extends PropertyReader, PropertyWriter { +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/PropertyReader.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/PropertyReader.java b/src/main/groovy/groovy/beans/PropertyReader.java new file mode 100644 index 0000000..4ef13af --- /dev/null +++ b/src/main/groovy/groovy/beans/PropertyReader.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.beans; + +/** + * @author Andres Almiray + */ +public interface PropertyReader { + Object read(Object owner, String propertyName); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/PropertyWriter.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/PropertyWriter.java b/src/main/groovy/groovy/beans/PropertyWriter.java new file mode 100644 index 0000000..5b9e1ce --- /dev/null +++ b/src/main/groovy/groovy/beans/PropertyWriter.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.beans; + +/** + * @author Andres Almiray + */ +public interface PropertyWriter { + void write(Object owner, String propertyName, Object value); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/Vetoable.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/Vetoable.java b/src/main/groovy/groovy/beans/Vetoable.java new file mode 100644 index 0000000..ff7d8de --- /dev/null +++ b/src/main/groovy/groovy/beans/Vetoable.java @@ -0,0 +1,113 @@ +/* + * 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.beans; + +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a groovy property or a class. + * <p> + * When annotating a property it indicates that the property should be a + * constrained property according to the JavaBeans spec, subject to + * listeners vetoing the property change. + * <p> + * When annotating a class it indicates that all groovy properties in that + * class should be bound as though each property had the annotation (even + * if it already has it explicitly). + * <p> + * It is a compilation error to place this annotation on a field (that is + * not a property, i.e. has scope visibility modifiers). + * <p> + * If a property with a user defined setter method is annotated the code + * block is wrapped with the needed code to fire off the event. + * <p> + * Here is a simple example of how to annotate a class with Vetoable: + * <pre> + * @groovy.beans.Vetoable + * class Person { + * String firstName + * def zipCode + * } + * </pre> + * This code is transformed by the compiler into something resembling the following + * snippet. Notice the difference between a String and a def/Object property: + * <pre> + * public class Person implements groovy.lang.GroovyObject { + * private java.lang.String firstName + * private java.lang.Object zipCode + * final private java.beans.VetoableChangeSupport this$vetoableChangeSupport + * + * public Person() { + * this$vetoableChangeSupport = new java.beans.VetoableChangeSupport(this) + * } + * + * public void addVetoableChangeListener(java.beans.VetoableChangeListener listener) { + * this$vetoableChangeSupport.addVetoableChangeListener(listener) + * } + * + * public void addVetoableChangeListener(java.lang.String name, java.beans.VetoableChangeListener listener) { + * this$vetoableChangeSupport.addVetoableChangeListener(name, listener) + * } + * + * public void removeVetoableChangeListener(java.beans.VetoableChangeListener listener) { + * this$vetoableChangeSupport.removeVetoableChangeListener(listener) + * } + * + * public void removeVetoableChangeListener(java.lang.String name, java.beans.VetoableChangeListener listener) { + * this$vetoableChangeSupport.removeVetoableChangeListener(name, listener) + * } + * + * public void fireVetoableChange(java.lang.String name, java.lang.Object oldValue, java.lang.Object newValue) throws java.beans.PropertyVetoException { + * this$vetoableChangeSupport.fireVetoableChange(name, oldValue, newValue) + * } + * + * public java.beans.VetoableChangeListener[] getVetoableChangeListeners() { + * return this$vetoableChangeSupport.getVetoableChangeListeners() + * } + * + * public java.beans.VetoableChangeListener[] getVetoableChangeListeners(java.lang.String name) { + * return this$vetoableChangeSupport.getVetoableChangeListeners(name) + * } + * + * public void setFirstName(java.lang.String value) throws java.beans.PropertyVetoException { + * this.fireVetoableChange('firstName', firstName, value) + * firstName = value + * } + * + * public void setZipCode(java.lang.Object value) throws java.beans.PropertyVetoException { + * this.fireVetoableChange('zipCode', zipCode, value) + * zipCode = value + * } + * } + * </pre> + * + * @see VetoableASTTransformation + * @author Danno Ferrin (shemnon) + */ [email protected] +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.FIELD, ElementType.TYPE}) +@GroovyASTTransformationClass("groovy.beans.VetoableASTTransformation") +public @interface Vetoable { +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/beans/VetoableASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/beans/VetoableASTTransformation.java b/src/main/groovy/groovy/beans/VetoableASTTransformation.java new file mode 100644 index 0000000..983e8da --- /dev/null +++ b/src/main/groovy/groovy/beans/VetoableASTTransformation.java @@ -0,0 +1,444 @@ +/* + * 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.beans; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.ast.tools.PropertyNodeUtils; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.SimpleMessage; +import org.codehaus.groovy.control.messages.SyntaxErrorMessage; +import org.codehaus.groovy.runtime.MetaClassHelper; +import org.codehaus.groovy.syntax.SyntaxException; +import org.codehaus.groovy.transform.GroovyASTTransformation; +import org.objectweb.asm.Opcodes; + +import java.beans.PropertyVetoException; +import java.beans.VetoableChangeListener; +import java.beans.VetoableChangeSupport; + +import static org.codehaus.groovy.ast.tools.GeneralUtils.args; +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.declS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.fieldX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; + +/** + * Handles generation of code for the {@code @Vetoable} annotation, and {@code @Bindable} + * if also present. + * <p> + * Generally, it adds (if needed) a VetoableChangeSupport field and + * the needed add/removeVetoableChangeListener methods to support the + * listeners. + * <p> + * It also generates the setter and wires the setter through the + * VetoableChangeSupport. + * <p> + * If a {@link Bindable} annotation is detected it also adds support similar + * to what {@link BindableASTTransformation} would do. + * + * @author Danno Ferrin (shemnon) + * @author Chris Reeves + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class VetoableASTTransformation extends BindableASTTransformation { + + protected static final ClassNode constrainedClassNode = ClassHelper.make(Vetoable.class); + + /** + * Convenience method to see if an annotated node is {@code @Vetoable}. + * + * @param node the node to check + * @return true if the node is constrained + */ + public static boolean hasVetoableAnnotation(AnnotatedNode node) { + for (AnnotationNode annotation : node.getAnnotations()) { + if (constrainedClassNode.equals(annotation.getClassNode())) { + return true; + } + } + return false; + } + + /** + * Handles the bulk of the processing, mostly delegating to other methods. + * + * @param nodes the AST nodes + * @param source the source unit for the nodes + */ + public void visit(ASTNode[] nodes, SourceUnit source) { + if (!(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { + throw new RuntimeException("Internal error: wrong types: $node.class / $parent.class"); + } + AnnotationNode node = (AnnotationNode) nodes[0]; + + if (nodes[1] instanceof ClassNode) { + addListenerToClass(source, (ClassNode) nodes[1]); + } else { + if ((((FieldNode)nodes[1]).getModifiers() & Opcodes.ACC_FINAL) != 0) { + source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( + new SyntaxException("@groovy.beans.Vetoable cannot annotate a final property.", + node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), + source)); + } + + addListenerToProperty(source, node, (AnnotatedNode) nodes[1]); + } + } + + private void addListenerToProperty(SourceUnit source, AnnotationNode node, AnnotatedNode parent) { + ClassNode declaringClass = parent.getDeclaringClass(); + FieldNode field = ((FieldNode) parent); + String fieldName = field.getName(); + for (PropertyNode propertyNode : declaringClass.getProperties()) { + boolean bindable = BindableASTTransformation.hasBindableAnnotation(parent) + || BindableASTTransformation.hasBindableAnnotation(parent.getDeclaringClass()); + + if (propertyNode.getName().equals(fieldName)) { + if (field.isStatic()) { + //noinspection ThrowableInstanceNeverThrown + source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( + new SyntaxException("@groovy.beans.Vetoable cannot annotate a static property.", + node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), + source)); + } else { + createListenerSetter(source, bindable, declaringClass, propertyNode); + } + return; + } + } + //noinspection ThrowableInstanceNeverThrown + source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( + new SyntaxException("@groovy.beans.Vetoable must be on a property, not a field. Try removing the private, protected, or public modifier.", + node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), + source)); + } + + + private void addListenerToClass(SourceUnit source, ClassNode classNode) { + boolean bindable = BindableASTTransformation.hasBindableAnnotation(classNode); + for (PropertyNode propertyNode : classNode.getProperties()) { + if (!hasVetoableAnnotation(propertyNode.getField()) + && !propertyNode.getField().isFinal() + && !propertyNode.getField().isStatic()) + { + createListenerSetter(source, + bindable || BindableASTTransformation.hasBindableAnnotation(propertyNode.getField()), + classNode, propertyNode); + } + } + } + + /** + * Wrap an existing setter. + */ + private static void wrapSetterMethod(ClassNode classNode, boolean bindable, String propertyName) { + String getterName = "get" + MetaClassHelper.capitalize(propertyName); + MethodNode setter = classNode.getSetterMethod("set" + MetaClassHelper.capitalize(propertyName)); + + if (setter != null) { + // Get the existing code block + Statement code = setter.getCode(); + + Expression oldValue = varX("$oldValue"); + Expression newValue = varX("$newValue"); + Expression proposedValue = varX(setter.getParameters()[0].getName()); + BlockStatement block = new BlockStatement(); + + // create a local variable to hold the old value from the getter + block.addStatement(declS(oldValue, callThisX(getterName))); + + // add the fireVetoableChange method call + block.addStatement(stmt(callThisX("fireVetoableChange", args( + constX(propertyName), oldValue, proposedValue)))); + + // call the existing block, which will presumably set the value properly + block.addStatement(code); + + if (bindable) { + // get the new value to emit in the event + block.addStatement(declS(newValue, callThisX(getterName))); + + // add the firePropertyChange method call + block.addStatement(stmt(callThisX("firePropertyChange", args(constX(propertyName), oldValue, newValue)))); + } + + // replace the existing code block with our new one + setter.setCode(block); + } + } + + private void createListenerSetter(SourceUnit source, boolean bindable, ClassNode declaringClass, PropertyNode propertyNode) { + if (bindable && needsPropertyChangeSupport(declaringClass, source)) { + addPropertyChangeSupport(declaringClass); + } + if (needsVetoableChangeSupport(declaringClass, source)) { + addVetoableChangeSupport(declaringClass); + } + String setterName = "set" + MetaClassHelper.capitalize(propertyNode.getName()); + if (declaringClass.getMethods(setterName).isEmpty()) { + Expression fieldExpression = fieldX(propertyNode.getField()); + BlockStatement setterBlock = new BlockStatement(); + setterBlock.addStatement(createConstrainedStatement(propertyNode, fieldExpression)); + if (bindable) { + setterBlock.addStatement(createBindableStatement(propertyNode, fieldExpression)); + } else { + setterBlock.addStatement(createSetStatement(fieldExpression)); + } + + // create method void <setter>(<type> fieldName) + createSetterMethod(declaringClass, propertyNode, setterName, setterBlock); + } else { + wrapSetterMethod(declaringClass, bindable, propertyNode.getName()); + } + } + + /** + * Creates a statement body similar to: + * <code>this.fireVetoableChange("field", field, field = value)</code> + * + * @param propertyNode the field node for the property + * @param fieldExpression a field expression for setting the property value + * @return the created statement + */ + protected Statement createConstrainedStatement(PropertyNode propertyNode, Expression fieldExpression) { + return stmt(callThisX("fireVetoableChange", args(constX(propertyNode.getName()), fieldExpression, varX("value")))); + } + + /** + * Creates a statement body similar to: + * <code>field = value</code>. + * <p> + * Used when the field is not also {@code @Bindable}. + * + * @param fieldExpression a field expression for setting the property value + * @return the created statement + */ + protected Statement createSetStatement(Expression fieldExpression) { + return assignS(fieldExpression, varX("value")); + } + + /** + * Snoops through the declaring class and all parents looking for a field + * of type VetoableChangeSupport. Remembers the field and returns false + * if found otherwise returns true to indicate that such support should + * be added. + * + * @param declaringClass the class to search + * @return true if vetoable change support should be added + */ + protected boolean needsVetoableChangeSupport(ClassNode declaringClass, SourceUnit sourceUnit) { + boolean foundAdd = false, foundRemove = false, foundFire = false; + ClassNode consideredClass = declaringClass; + while (consideredClass!= null) { + for (MethodNode method : consideredClass.getMethods()) { + // just check length, MOP will match it up + foundAdd = foundAdd || method.getName().equals("addVetoableChangeListener") && method.getParameters().length == 1; + foundRemove = foundRemove || method.getName().equals("removeVetoableChangeListener") && method.getParameters().length == 1; + foundFire = foundFire || method.getName().equals("fireVetoableChange") && method.getParameters().length == 3; + if (foundAdd && foundRemove && foundFire) { + return false; + } + } + consideredClass = consideredClass.getSuperClass(); + } + // check if a super class has @Vetoable annotations + consideredClass = declaringClass.getSuperClass(); + while (consideredClass!=null) { + if (hasVetoableAnnotation(consideredClass)) return false; + for (FieldNode field : consideredClass.getFields()) { + if (hasVetoableAnnotation(field)) return false; + } + consideredClass = consideredClass.getSuperClass(); + } + if (foundAdd || foundRemove || foundFire) { + sourceUnit.getErrorCollector().addErrorAndContinue( + new SimpleMessage("@Vetoable cannot be processed on " + + declaringClass.getName() + + " because some but not all of addVetoableChangeListener, removeVetoableChange, and fireVetoableChange were declared in the current or super classes.", + sourceUnit) + ); + return false; + } + return true; + } + + /** + * Creates a setter method with the given body. + * <p> + * This differs from normal setters in that we need to add a declared + * exception java.beans.PropertyVetoException + * + * @param declaringClass the class to which we will add the setter + * @param propertyNode the field to back the setter + * @param setterName the name of the setter + * @param setterBlock the statement representing the setter block + */ + protected void createSetterMethod(ClassNode declaringClass, PropertyNode propertyNode, String setterName, Statement setterBlock) { + ClassNode[] exceptions = {ClassHelper.make(PropertyVetoException.class)}; + MethodNode setter = new MethodNode( + setterName, + PropertyNodeUtils.adjustPropertyModifiersForMethod(propertyNode), + ClassHelper.VOID_TYPE, + params(param(propertyNode.getType(), "value")), + exceptions, + setterBlock); + setter.setSynthetic(true); + // add it to the class + declaringClass.addMethod(setter); + } + + /** + * Adds the necessary field and methods to support vetoable change support. + * <p> + * Adds a new field: + * <code>"protected final java.beans.VetoableChangeSupport this$vetoableChangeSupport = new java.beans.VetoableChangeSupport(this)"</code> + * <p> + * Also adds support methods: + * <code>public void addVetoableChangeListener(java.beans.VetoableChangeListener)</code> + * <code>public void addVetoableChangeListener(String, java.beans.VetoableChangeListener)</code> + * <code>public void removeVetoableChangeListener(java.beans.VetoableChangeListener)</code> + * <code>public void removeVetoableChangeListener(String, java.beans.VetoableChangeListener)</code> + * <code>public java.beans.VetoableChangeListener[] getVetoableChangeListeners()</code> + * + * @param declaringClass the class to which we add the support field and methods + */ + protected void addVetoableChangeSupport(ClassNode declaringClass) { + ClassNode vcsClassNode = ClassHelper.make(VetoableChangeSupport.class); + ClassNode vclClassNode = ClassHelper.make(VetoableChangeListener.class); + + // add field: + // protected static VetoableChangeSupport this$vetoableChangeSupport = new java.beans.VetoableChangeSupport(this) + FieldNode vcsField = declaringClass.addField( + "this$vetoableChangeSupport", + ACC_FINAL | ACC_PRIVATE | ACC_SYNTHETIC, + vcsClassNode, + ctorX(vcsClassNode, args(varX("this")))); + + // add method: + // void addVetoableChangeListener(listener) { + // this$vetoableChangeSupport.addVetoableChangeListener(listener) + // } + declaringClass.addMethod( + new MethodNode( + "addVetoableChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(vclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(vcsField), "addVetoableChangeListener", args(varX("listener", vclClassNode)))))); + + // add method: + // void addVetoableChangeListener(name, listener) { + // this$vetoableChangeSupport.addVetoableChangeListener(name, listener) + // } + declaringClass.addMethod( + new MethodNode( + "addVetoableChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(ClassHelper.STRING_TYPE, "name"), param(vclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(vcsField), "addVetoableChangeListener", args(varX("name", ClassHelper.STRING_TYPE), varX("listener", vclClassNode)))))); + + // add method: + // boolean removeVetoableChangeListener(listener) { + // return this$vetoableChangeSupport.removeVetoableChangeListener(listener); + // } + declaringClass.addMethod( + new MethodNode( + "removeVetoableChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(vclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(vcsField), "removeVetoableChangeListener", args(varX("listener", vclClassNode)))))); + + // add method: void removeVetoableChangeListener(name, listener) + declaringClass.addMethod( + new MethodNode( + "removeVetoableChangeListener", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(ClassHelper.STRING_TYPE, "name"), param(vclClassNode, "listener")), + ClassNode.EMPTY_ARRAY, + stmt(callX(fieldX(vcsField), "removeVetoableChangeListener", args(varX("name", ClassHelper.STRING_TYPE), varX("listener", vclClassNode)))))); + + // add method: + // void fireVetoableChange(String name, Object oldValue, Object newValue) + // throws PropertyVetoException + // { + // this$vetoableChangeSupport.fireVetoableChange(name, oldValue, newValue) + // } + declaringClass.addMethod( + new MethodNode( + "fireVetoableChange", + ACC_PUBLIC, + ClassHelper.VOID_TYPE, + params(param(ClassHelper.STRING_TYPE, "name"), param(ClassHelper.OBJECT_TYPE, "oldValue"), param(ClassHelper.OBJECT_TYPE, "newValue")), + new ClassNode[] {ClassHelper.make(PropertyVetoException.class)}, + stmt(callX(fieldX(vcsField), "fireVetoableChange", args(varX("name", ClassHelper.STRING_TYPE), varX("oldValue"), varX("newValue")))))); + + // add method: + // VetoableChangeListener[] getVetoableChangeListeners() { + // return this$vetoableChangeSupport.getVetoableChangeListeners + // } + declaringClass.addMethod( + new MethodNode( + "getVetoableChangeListeners", + ACC_PUBLIC, + vclClassNode.makeArray(), + Parameter.EMPTY_ARRAY, + ClassNode.EMPTY_ARRAY, + returnS(callX(fieldX(vcsField), "getVetoableChangeListeners")))); + + // add method: + // VetoableChangeListener[] getVetoableChangeListeners(String name) { + // return this$vetoableChangeSupport.getVetoableChangeListeners(name) + // } + declaringClass.addMethod( + new MethodNode( + "getVetoableChangeListeners", + ACC_PUBLIC, + vclClassNode.makeArray(), + params(param(ClassHelper.STRING_TYPE, "name")), + ClassNode.EMPTY_ARRAY, + returnS(callX(fieldX(vcsField), "getVetoableChangeListeners", args(varX("name", ClassHelper.STRING_TYPE)))))); + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/cli/CliBuilderException.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/cli/CliBuilderException.groovy b/src/main/groovy/groovy/cli/CliBuilderException.groovy new file mode 100644 index 0000000..84a9438 --- /dev/null +++ b/src/main/groovy/groovy/cli/CliBuilderException.groovy @@ -0,0 +1,24 @@ +/* + * 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.cli + +import groovy.transform.InheritConstructors + +@InheritConstructors +class CliBuilderException extends RuntimeException { } http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/cli/Option.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/cli/Option.java b/src/main/groovy/groovy/cli/Option.java new file mode 100644 index 0000000..9b48861 --- /dev/null +++ b/src/main/groovy/groovy/cli/Option.java @@ -0,0 +1,105 @@ +/* + * 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.cli; + +import groovy.transform.Undefined; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a method or property can be used to set a CLI option. + */ [email protected] +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface Option { + /** + * The description of this option + * + * @return the description of this option + */ + String description() default ""; + + /** + * The short name of this option. Defaults to the name of member being annotated if the longName is empty. + * + * @return the short name of this option + */ + String shortName() default ""; + + /** + * The long name of this option. Defaults to the name of member being annotated. + * + * @return the long name of this option + */ + String longName() default ""; + + /** + * The value separator for this multi-valued option. Only allowed for array-typed arguments. + * + * @return the value separator for this multi-valued option + */ + String valueSeparator() default ""; + + /** + * Whether this option can have an optional argument. + * Only supported for array-typed arguments to indicate that the array may be empty. + * + * @return true if this array-typed option can have an optional argument (i.e. could be empty) + */ + boolean optionalArg() default false; + + /** + * How many arguments this option has. + * A value greater than 1 is only allowed for array-typed arguments. + * Ignored for boolean options which are assumed to have a default of 0 + * or if {@code numberOfArgumentsString} is set. + * + * @return the number of arguments + */ + int numberOfArguments() default 1; + + /** + * How many arguments this option has represented as a String. + * Only allowed for array-typed arguments. + * Overrides {@code numberOfArguments} if set. + * The special values of '+' means one or more and '*' as 0 or more. + * + * @return the number of arguments (as a String) + */ + String numberOfArgumentsString() default ""; + + /** + * The default value for this option as a String; subject to type conversion and 'convert'. + * Ignored for Boolean options. + * + * @return the default value for this option + */ + String defaultValue() default ""; + + /** + * A conversion closure to convert the incoming String into the desired object + * + * @return the closure to convert this option's argument(s) + */ + Class convert() default Undefined.CLASS.class; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/cli/OptionField.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/cli/OptionField.groovy b/src/main/groovy/groovy/cli/OptionField.groovy new file mode 100644 index 0000000..69cc1f5 --- /dev/null +++ b/src/main/groovy/groovy/cli/OptionField.groovy @@ -0,0 +1,27 @@ +/* + * 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.cli + +import groovy.transform.AnnotationCollector +import groovy.transform.Field + +@Option +@Field +@AnnotationCollector +@interface OptionField { } \ No newline at end of file
