You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2017/12/20 00:56:41 UTC
[44/49] groovy git commit: Move source files to proper packages
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)
+ */
+@java.lang.annotation.Documented
+@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.
+ */
+@java.lang.annotation.Documented
+@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