Project: Cinch
//   Copyright 2011 Palantir Technologies
// 
//   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 com.palantir.ptoss.cinch.swing; 
 
import java.awt.event.ActionEvent; 
import java.awt.event.ActionListener; 
import java.lang.annotation.ElementType; 
import java.lang.annotation.Retention; 
import java.lang.annotation.RetentionPolicy; 
import java.lang.annotation.Target; 
import java.lang.reflect.Field; 
import java.lang.reflect.InvocationTargetException; 
import java.lang.reflect.Method; 
import java.util.Collection; 
import java.util.List; 
 
import javax.swing.JButton; 
import javax.swing.JTextField; 
 
import org.apache.log4j.Logger; 
 
import com.google.common.collect.ImmutableList; 
import com.palantir.ptoss.cinch.core.Bindable; 
import com.palantir.ptoss.cinch.core.BindableModel; 
import com.palantir.ptoss.cinch.core.Binding; 
import com.palantir.ptoss.cinch.core.BindingContext; 
import com.palantir.ptoss.cinch.core.BindingException; 
import com.palantir.ptoss.cinch.core.BindingWiring; 
import com.palantir.ptoss.cinch.core.Bindings; 
import com.palantir.ptoss.cinch.core.ObjectFieldMethod; 
 
/**
 * <p> 
 * A component binding that will call a method when an action occurs.  This can be applied to 
 * any object that has an "addActionListener" method that takes an {@link ActionListener}. 
 * Normally used for any sort of {@link JButton} or for a {@link JTextField}. 
 * </p> 
 * Here's an example of using @Action annotations: 
 * <pre> 
 * <code> 
 * public class BoundJCheckBoxExample { 
 * 
 *     public static class Model extends DefaultBindableModel { 
 *         private boolean state; 
 * 
 *         public void setState(boolean state) { 
 *             this.state = state; 
 *             update(); 
 *         } 
 * 
 *         public boolean isState() { 
 *             return state; 
 *         } 
*      } 
 * 
 *     public static class Controller { 
 *         private final Model model; 
 * 
 *         public Controller(Model model) { 
 *             this.model = model; 
 *         } 
 * 
 *         public void setToTrue() { 
 *             model.setState(true); 
 *         } 
 * 
 *         public void setToFalse() { 
 *             model.setState(false); 
 *         } 
 *     } 
 * 
 *     private final Model model = new Model(); 
 *     @Bindable 
 *     private final Controller controller = new Controller(model); 
 * 
 *     @Bound(to = "state") 
 *     private final JCheckBox box = new JCheckBox("State"); 
 *     @Bound(to = "state") 
 *     private final JLabel stateLabel = new JLabel("?"); 
 * 
 *     @Action(call = "setToTrue") 
 *     private final JButton trueButton = new JButton("Set True"); 
 *     @Action(call = "setToFalse") 
 *     private final JButton falseButton = new JButton("Set False"); 
 * 
 *     private final JPanel panel = new JPanel(); 
 * 
 *     private final Bindings bindings = Bindings.standard(); 
 * 
 *     public BoundJCheckBoxExample() { 
 *            // ... layout panel ... 
 *            bindings.bind(this); 
 *     } 
 * 
 *     // main() 
 * } 
 * </pre></code> 
 * 
 * <h2>Methods available to @Actions</h2> 
 * <p>The following are callable from @Actions: 
 * <ul> 
 * <li>Methods on the class passed into {@link Bindings#bind(Object)} if the class is marked as 
 * {@link Bindable}.</li> 
 * <li>Any methods in fields that are marked {@link Bindable} (as in the example).</li> 
 * <li>Any methods in fields that implement {@link BindableModel}.</li> 
 * </ul> 
 * <h2>Disambiguation</h2> 
 * <p> 
 * Strings passed into {@link #call()} are disambiguated as follows: 
 * </p> 
 * <ol> 
 * <li>If the string uniquely identifies a model property or bindable method then 
 * there is no problem.</li> 
 * <li>If there are two or more possibilities then you must specify the field name in the 
 * string like "model1.state" or "fileController.close".</li> 
 * <li>You can always use the long form in (2) even if the short form would suffice.</li> 
 * </ol> 
 * 
 * @see Bound 
 */
 
@Retention(RetentionPolicy.RUNTIME) 
@Target({ElementType.FIELD}) 
public @interface Action { 
    /**
     * The name of the method to call when the action occurs. Must be accessible in the 
     * {@link BindingContext}. 
     */
 
    String call(); 
 
    /**
     * Inner utility class used to wire {@link Action} bindings. 
     */
 
    static class Wiring implements BindingWiring { 
        private static final Logger logger = Logger.getLogger(Action.class); 
 
        /**
         * Wires all {@link Action} bindings in the passed context. 
         * Called by {@link Bindings#createBindings(BindingContext)} as part of runtime wiring 
         * process. 
         * 
         * @param context 
         */
 
        public Collection<Binding> wire(BindingContext context) { 
            List<Field> actions = context.getAnnotatedFields(Action.class); 
            for (Field field : actions) { 
                Action action = field.getAnnotation(Action.class); 
                String call = action.call(); 
                try { 
                    wire(call, field, context); 
                } catch (Exception e) { 
                    throw new BindingException("could not wire up @Action on " + 
                            field.getName(), e); 
                } 
            } 
            return ImmutableList.of(); 
        } 
 
        /**
         * Wires up to any object with an addActionListener method.  Automatically called 
         * by {@link #wire(BindingContext)}. 
         * 
         * @param call name of an {@link ObjectFieldMethod} in the passed {@link BindingContext}. 
         * @param field field to bind the call to. 
         * @param context the {@link BindingContext} 
         */
 
        private static void wire(String call, Field field, BindingContext context) 
                throws SecurityException, NoSuchMethodException, IllegalArgumentException, 
                        IllegalAccessException, InvocationTargetException { 
            Method aalMethod = field.getType().getMethod("addActionListener", ActionListener.class); 
            Object actionObject = context.getFieldObject(field, Object.class); 
            final ObjectFieldMethod ofm = context.getBindableMethod(call); 
            if (ofm == null) { 
                throw new BindingException("could not find bindable method: " + call); 
            } 
            ActionListener actionListener = new ActionListener() { 
                public void actionPerformed(ActionEvent e) { 
                    try { 
                        boolean accessible = ofm.getMethod().isAccessible(); 
                        ofm.getMethod().setAccessible(true); 
                        ofm.getMethod().invoke(ofm.getObject()); 
                        ofm.getMethod().setAccessible(accessible); 
                    } catch (InvocationTargetException itex) { 
                        logger.error("exception during action firing", itex.getCause()); 
                    } catch (Exception ex) { 
                        logger.error("exception during action firing", ex); 
                    } 
                } 
            }; 
            aalMethod.invoke(actionObject, actionListener); 
        } 
    } 
}