/* * Copyright 2008-2017 the original author or authors. * * Licensed 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 org.codehaus.griffon.compile.core.ast.transform; import griffon.core.Vetoable; import org.codehaus.griffon.compile.core.AnnotationHandler; import org.codehaus.griffon.compile.core.AnnotationHandlerFor; import org.codehaus.griffon.compile.core.VetoableConstants; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.FieldExpression; 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.ExpressionStatement; import org.codehaus.groovy.ast.stmt.Statement; 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.transform.GroovyASTTransformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.beans.VetoableChangeSupport; import java.lang.reflect.Modifier; import static griffon.util.GriffonNameUtils.getGetterName; import static griffon.util.GriffonNameUtils.getSetterName; import static java.lang.reflect.Modifier.FINAL; import static java.lang.reflect.Modifier.PROTECTED; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.NO_ARGS; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.NO_EXCEPTIONS; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.THIS; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.args; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.assign; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.call; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.constx; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.ctor; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.decls; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.field; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.injectField; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.injectInterface; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.injectMethod; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.param; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.params; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.stmnt; import static org.codehaus.griffon.compile.core.ast.GriffonASTUtils.var; import static org.codehaus.griffon.compile.core.ast.transform.ObservableASTTransformation.hasObservableAnnotation; import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE; import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE; import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE; /** * Handles generation of code for the {@code @Vetoable} annotation. * * @author Andres Almiray */ @AnnotationHandlerFor(griffon.transform.Vetoable.class) @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) public class VetoableASTTransformation extends AbstractASTTransformation implements VetoableConstants, AnnotationHandler { private static final Logger LOG = LoggerFactory.getLogger(VetoableASTTransformation.class); private static final ClassNode VETOABLE_CNODE = makeClassSafe(Vetoable.class); private static final ClassNode PROPERTY_VETO_EXCEPTION_CNODE = makeClassSafe(PropertyVetoException.class); private static final ClassNode VETOABLE_ANNOTATION_CNODE = makeClassSafe(griffon.transform.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 an event publisher */ public static boolean hasVetoableAnnotation(AnnotatedNode node) { for (AnnotationNode annotation : node.getAnnotations()) { if (VETOABLE_ANNOTATION_CNODE.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) { addVetoableIfNeeded(source, (ClassNode) nodes[1]); } else { if ((((FieldNode) nodes[1]).getModifiers() & Modifier.FINAL) != 0) { source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( new SyntaxException("@griffon.transform.Vetoable cannot annotate a final property.", node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), source)); } addVetoableIfNeeded(source, node, (AnnotatedNode) nodes[1]); } } public static boolean needsVetoableSupport(ClassNode classNode, SourceUnit source) { return needsDelegate(classNode, source, VETOABLE_METHODS, "Vetoable", VETOABLE_TYPE); } public static void addVetoableIfNeeded(SourceUnit source, ClassNode classNode) { if (needsVetoableSupport(classNode, source)) { LOG.debug("Injecting {} into {}", VETOABLE_TYPE, classNode.getName()); apply(classNode); } boolean bindable = hasObservableAnnotation(classNode); for (PropertyNode propertyNode : classNode.getProperties()) { if (!hasVetoableAnnotation(propertyNode.getField()) && ((propertyNode.getField().getModifiers() & Modifier.FINAL) == 0) && !propertyNode.getField().isStatic()) { createListenerSetter(source, bindable || hasObservableAnnotation(propertyNode.getField()), classNode, propertyNode); } } } private void addVetoableIfNeeded(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 = hasObservableAnnotation(parent) || hasObservableAnnotation(parent.getDeclaringClass()); if (propertyNode.getName().equals(fieldName)) { if (field.isStatic()) { source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( new SyntaxException("@griffon.transform.Vetoable cannot annotate a static property.", node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber()), source)); } else { createListenerSetter(source, bindable, declaringClass, propertyNode); } return; } } source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( new SyntaxException("@griffon.transform.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 static void createListenerSetter(SourceUnit source, boolean bindable, ClassNode declaringClass, PropertyNode propertyNode) { //if (bindable || needsObservableSupport(declaringClass, source)) { // ObservableASTTransformation.apply(declaringClass); //} if (needsVetoableSupport(declaringClass, source)) { apply(declaringClass); } String setterName = getSetterName(propertyNode.getName()); if (declaringClass.getMethods(setterName).isEmpty()) { Expression fieldExpression = new FieldExpression(propertyNode.getField()); BlockStatement setterBlock = new BlockStatement(); setterBlock.addStatement(createConstrainedStatement(propertyNode, fieldExpression)); // if (bindable) { setterBlock.addStatement(createBindableStatement(propertyNode, fieldExpression)); // } else { // setterBlock.addStatement(assigns(fieldExpression, var(VALUE))); // } // create method void <setter>(<type> fieldName) createSetterMethod(declaringClass, propertyNode, setterName, setterBlock); } else { wrapSetterMethod(declaringClass, bindable, propertyNode.getName()); } } private static Statement createConstrainedStatement(PropertyNode propertyNode, Expression fieldExpression) { return stmnt(call( THIS, METHOD_FIRE_VETOABLE_CHANGE, args(constx(propertyNode.getName()), fieldExpression, var(VALUE)))); } private static Statement createBindableStatement(PropertyNode propertyNode, Expression fieldExpression) { // create statementBody return stmnt(call( THIS, METHOD_FIRE_PROPERTY_CHANGE, args( constx(propertyNode.getName()), fieldExpression, assign(fieldExpression, var(VALUE))))); } private static void createSetterMethod(ClassNode declaringClass, PropertyNode propertyNode, String setterName, Statement setterBlock) { MethodNode setter = new MethodNode( setterName, propertyNode.getModifiers(), VOID_TYPE, params(param(propertyNode.getType(), VALUE)), new ClassNode[]{PROPERTY_VETO_EXCEPTION_CNODE}, setterBlock); setter.setSynthetic(true); // add it to the class declaringClass.addMethod(setter); } private static void wrapSetterMethod(ClassNode classNode, boolean bindable, String propertyName) { String getterName = getGetterName(propertyName); MethodNode setter = classNode.getSetterMethod(getSetterName(propertyName)); if (setter != null) { // Get the existing code block Statement code = setter.getCode(); VariableExpression oldValue = new VariableExpression("$oldValue"); VariableExpression newValue = new VariableExpression("$newValue"); VariableExpression proposedValue = new VariableExpression(setter.getParameters()[0].getName()); BlockStatement block = new BlockStatement(); // create a local variable to hold the old value from the getter block.addStatement(decls(oldValue, call(THIS, getterName, NO_ARGS))); // add the fireVetoableChange method call block.addStatement(new ExpressionStatement(new MethodCallExpression( THIS, METHOD_FIRE_VETOABLE_CHANGE, 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, call(THIS, getterName, NO_ARGS))); // add the firePropertyChange method call block.addStatement(new ExpressionStatement(new MethodCallExpression( THIS, METHOD_FIRE_PROPERTY_CHANGE, args(constx(propertyName), oldValue, newValue)))); } // replace the existing code block with our new one setter.setCode(block); } } /** * Adds the necessary field and methods to support resource locating. * * @param classNode the class to which we add the support field and methods */ public static void apply(ClassNode classNode) { if( ObservableASTTransformation.needsObservableSupport(classNode, classNode.getModule().getContext())) { ObservableASTTransformation.apply(classNode); } injectInterface(classNode, VETOABLE_CNODE); ClassNode vcsClassNode = makeClassSafe(VetoableChangeSupport.class); ClassNode pceClassNode = makeClassSafe(PropertyChangeEvent.class); // add field: // protected final VetoableChangeSupport this$vetoableChangeSupport = new java.beans.VetoableChangeSupport(this) FieldNode vcsField = injectField(classNode, VETOABLE_CHANGE_SUPPORT_FIELD_NAME, FINAL | PROTECTED, vcsClassNode, ctor(vcsClassNode, args(THIS))); addDelegateMethods(classNode, VETOABLE_CNODE, new FieldExpression(vcsField)); // add method: // void firePropertyChange(String name, Object oldValue, Object newValue) { // this$vetoableChangeSupport.firePropertyChange(name, oldValue, newValue) // } injectMethod(classNode, new MethodNode( METHOD_FIRE_VETOABLE_CHANGE, PROTECTED, VOID_TYPE, params( param(STRING_TYPE, NAME), param(makeClassSafe(OBJECT_TYPE), OLD_VALUE), param(OBJECT_TYPE, NEW_VALUE)), NO_EXCEPTIONS, stmnt(call( field(vcsField), METHOD_FIRE_VETOABLE_CHANGE, args(var(NAME), var(OLD_VALUE), var(NEW_VALUE)))))); // add method: // void firePropertyChange(PropertyChangeEvent event) { // this$vetoableChangeSupport.firePropertyChange(event) // } injectMethod(classNode, new MethodNode( METHOD_FIRE_VETOABLE_CHANGE, PROTECTED, VOID_TYPE, params(param(pceClassNode, EVENT)), NO_EXCEPTIONS, stmnt(call( field(vcsField), METHOD_FIRE_VETOABLE_CHANGE, args(var(EVENT)))))); } }