// WeakProxyReference.java

import java.lang.ref.*;
import java.lang.reflect.*;

//These imports are just for the test code used by the main method
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

/**
 * DESCRIPTION<p>
 * This class contains a static <code>create</code> method which creates a
 * proxy for a given object and adds it to a given object to a given container
 * in such a way that all the methods invoked on that proxy by the container 
 * will be forwarded to the original object. 
 * It should work exactly as if the object had been added directly to the 
 * container but with one important difference:
 * No strong reference to the object will be created in the process. This
 * allows the object <i>and any resources that object references</i> to be
 * garbage collected when no other parts of your application reference it.
 * This helps eliminate one of the most common sources of memory leaks usually
 * involving listener objects that keep their referenced windows, etc. from
 * being garbage collected <i>without requiring you to figure out when to
 * remove those listeners yourself</i>. In other words, it makes it possible
 * to "set and forget" your listeners.<p>
 * 
 * The only caveat is that you will need to make sure that there is at least
 * one strong reference to your listener objects in your application, 
 * otherwise they will now be immediately garbage collectable. 
 * That's because you can no longer count on the container holding a strong 
 * reference to your listener. The cleanest solution is to make the thing being
 * referenced by the listener (rather than the container which would have held
 * the listener) keep any sort of strong reference to that listener. 
 * This sort of circular reference will keep both resource and listener alive
 * until no other parts of your application hold references to the resource.<p>
 *
 * EXAMPLE
 * <pre><code>
 * final JPanel my_panel = new JPanel(); // the expensive resource
 * ActionListener my_listener = new ActionListener() {
 *     public void ActionPerformed(ActionEvent ae) {
 *         ...code that directly affects "my_panel"...};
 *     }
 * });
 * JButton my_button = new JButton("Panel Controller");
 * // use the following instead of my_button.addActionListener(my_listener);
 * WeakProxyReference.create(my_button, my_listener, ActionListener.class);
 * my_panel.putClientProperty(new Object(), my_listener); // circular reference
 * </code></pre>
 * 
 * @author Melinda Green - Superliminal Software
 */
public class WeakProxyReference {
    private WeakReference reference_to_held; // weak reference to held object
    private Object proxyForHeld;
    private final static boolean DEBUG = true;

    /**
     * Creates a weak proxy reference between two objects.
     *
     * @param holder is the object to which a weak proxy reference 
     * to the second parameter is to be added. 
     * 
     * @param held is the object which holder is supposed to reference.
     * 
     * @param heldBaseClass is the Class object for the <i>base class</i> 
     * of the held object. 
     * IOW, the class of object the holder is expecting to recieve. 
     * For example, if you want to add your own derived action listener 
     * to a button, you must pass ActionListener.class, 
     * <i>not</i> MyActionListener.class.
     *
     * @param addMethodName is the name of holder's method used to 
     * add objects of type <code>heldBaseClass</code>. 
     * 
     * @param removeMethodName is the name of holder's method used to 
     * remove objects of type <code>heldBaseClass</code>. 
     * 
     * <br><br><b>NOTE:</b> Because the object which you pass as the "held" 
     * parameter will not be strongly held by the holder, you will need to make
     * sure you keep at least one reference to it. 
     * The most natural place to do that is to store that reference is in a 
     * private member variable of whatever object it operates on. That sort
     * of circular reference is good because both objects will be garbage
     * collected together when there are no external references to either one.
     * Or more prosaically, this makes them bound by a mutual suicide pact.
     */
     public static void create(
            Object holder, 
            Object held,
            Class heldBaseClass,
            String addMethodName, 
            String removeMethodName) 
     {
         new WeakProxyReference(holder, held, heldBaseClass, 
                                addMethodName, removeMethodName);
     }

    /**
     * Shorthand version of <code>create</code> method which assumes
     * the add and remove method names are simply the heldBaseClass class name
     * with the strings "add" and "remove" prepended. For example, since the 
     * JButton class has "addActionListener" and "removeActionListener" 
     * methods, you can create a WeakProxyReference from a JButton to an 
     * instance of your custom action listener like this: <br><code>
     * WeakProxyReference.create(myButton, myListener, ActionListener.class);
     * </code><br>
     * Be sure to see "NOTE" in the 5 argument version of this method.
     */
    public static void create(Object holder, Object held, Class heldBaseClass) {
        String shortBaseName = heldBaseClass.getName().substring(
            heldBaseClass.getName().lastIndexOf('.')+1);
        create(holder,  
            held,
            heldBaseClass,
            "add"    + shortBaseName, 
            "remove" + shortBaseName);
        if(DEBUG) 
            System.out.println("Created a WeakProxyReference to a " +
                                shortBaseName);
    }
    
    /**
     * Constructor is private since there's nothing useful the user can do with
     * an object of this type.
     */
    private WeakProxyReference(
            final Object holder, 
            Object held,  
            final Class heldBaseClass,
            String addMethodName, 
            final String removeMethodName)
    {
        reference_to_held = new WeakReference(held);
        //Define the call-through behavior used by the proxy object we're about
        //to create which dispatches all method calls to the original target
        //object if it still exists, otherwise remove the unneeded proxy.
        InvocationHandler handler = new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) {
                String meth_name = method.getName();
                if(DEBUG)
                    System.out.println("invocation handler invoked for "+
                                        method.getName());
                Object original_held_object = reference_to_held.get();
                if(original_held_object == null)  {
                    //The equals() method is a special case
                    //because container remove methods called below use equals
                    //to find the element to remove, and because the object in 
                    //the collection is really a proxy object and not the 
                    //original held object. Normally we'd dispatch even the 
                    //equals method but since the held object is already gone,
                    //we need to make sure the container remove methods still 
                    //work.
                    if(meth_name.equals("equals") && proxy == proxyForHeld)
                        return new Boolean(args[0] instanceof Proxy);
                    //Because the weak reference object returned null, we know
                    //the original held object is now only weakly reachable 
                    //and is therefore garbage-collectable or even already 
                    //garbage collected. Just to be clean we take this 
                    //opportunity to remove the little proxy object from the 
                    //holder since its no longer useful and can also be garbage
                    //collected.
                    Method remover = getMethodFor(holder, 
                            removeMethodName, new Class[] { heldBaseClass });
                    if(DEBUG) System.out.println("\tremoving stale proxy");
                    doInvoke(remover, holder, new Object[] { proxyForHeld });
                    if(DEBUG) System.out.println("\tdone removing stale proxy");
                    return null; // nobody to dispatch to
                }
                Method held_object_method = getMethodFor( original_held_object,
                                meth_name, method.getParameterTypes());
                return doInvoke(held_object_method, original_held_object, args);
            }
        };
        //Create the proxy
        proxyForHeld = Proxy.newProxyInstance(
                                    held.getClass().getClassLoader(),
                                    new Class[] { heldBaseClass },
                                    handler);
        //Add the proxy to the holder by looking up its appropriate "add"
        //method and invoking it.
        Method adder = getMethodFor(holder, addMethodName, 
                            new Class[] { heldBaseClass }); 
        doInvoke(adder, holder, new Object[] { proxyForHeld });
    }

    /**
     * Helper function for invoking a given method on a given object
     * with exceptions caught and dealt with.
     */
    private static Object doInvoke(Method method, Object obj, Object[] args) {
        try { return method.invoke(obj, args); }
        catch (IllegalAccessException e) { printException(e); }
        catch (InvocationTargetException e) { printException(e); }
        catch (NullPointerException e) { printException(e); }
        return null;
    }

    /**
     * Helper function for getting a given method of a given object
     * with exceptions caught and dealt with.
     */
    private Method getMethodFor(Object obj, String method_name, 
                            Class parameterTypes[]) 
    {
        try { return obj.getClass().getMethod(method_name, parameterTypes); }
        catch (NoSuchMethodException e) { printException(e); }
        return null;
    }

    private static void printException(Exception e) {
        System.err.println("WeakListener exception " + e);
    }


    /**
     * A test method for WeakProxyReference.
     * Call with no arguments to run a simple example,
     * and with any extra argument to run a more complex example.
     */
    private static void main(String args[]) {
        if(args.length == 0)
            twoWindowTest();
        else
            buttonSelectionTest();
    }

    private static void twoWindowTest()  {
        // Make a window with a button that will control another window.
        JButton controller_button = new JButton(
            "Toggle other window's background");
        JFrame control_frame = new JFrame("Control Window");
        control_frame.getContentPane().add(controller_button);
        control_frame.setLocation(new Point(100, 100));
        control_frame.pack();
        control_frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Create the window to be controlled by the first one.
        final JFrame controlled_window = new JFrame(
            "Resource which needs to be garbage collectable");
        final JTextArea instructions = new JTextArea(
            "This window represents an expensive resource you want to be "+
            "garbage collectible after it's closed. Note that the JFrame is "+
            "set up to 'dospose on close', so clicking the X in the upper "+
            "right hand corner should do it. But first, click the toggle "+
            "button in the other window a few times to see that it has a "+
            "button listener which references this window. Then close this "+
            "window. To see the console message that the resource has indeed "+
            "been garbage collected depends upon when, if ever, your VM does "+
            "garbage collection. You will at least need to compile this code "+
            "with the DEBUG flag set to true to generate the print statements. "+
            "With the JDK 1.3 Windows VM I found that the garbage collector "+
            "will kick in if after I've closed this window, I maximize the "+
            "control window and then restore it to normal. When I then click "+
            "the toggle one more time I get the message showing that the "+
            "garbage collecter has done it's thing.");
        instructions.setLineWrap(true);
        instructions.setWrapStyleWord(true);
        controlled_window.getContentPane().add(instructions);
        controlled_window.setSize(new Dimension(450, 250));
        controlled_window.setLocation(new Point(400, 100));
        controlled_window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        // Create the controler which will affect the controled window.
        ActionListener toggler = new ActionListener()  {
            boolean yellow = false; // the current background color
            public void actionPerformed(ActionEvent ae) {
                System.out.println("colored window being toggled");
                yellow = !yellow;
                instructions.setBackground(yellow ? Color.yellow : Color.white);
                instructions.revalidate();
            }
        };

        //Rather than calling
        //controller_button.addActionListener(controlled_window.getController())
        //like we normally would, here's the magic call to set up
        //the weak proxy reference instead:
        WeakProxyReference.create(controller_button, toggler, 
            ActionListener.class);

        // Unfortunately replacing one call with another is not quite enough.
        // Here's the important line that circumvents the one drawback of
        // replacing a strong reference to the listener with a weak one.
        // We create a strong reference from the CONTROLLED object to the
        // listener that controls it. IOW a circular reference chain that's
        // garbage collectible when there are no external references left to
        // either of them which is the case after the controlled window is
        // closed. After that, both the controlled window and the control
        // listener are subject to garbage collection.
        // Note that you don't need to use putClientProperty to do this.
        // ANY call that would result in a strong reference to the listener
        // from any object that's only referenced within the controlled 
        // resource will do. For the common case of AWT component listeners,
        // the putClientProperty method is particularly useful as it provides
        // a way to associate a client object with *any* component. In this case
        // the toggler listener is associated with the instructions object
        // it affects. The object passed as the first argument is completely 
        // arbitrary since any unreferenced object will do.
        instructions.putClientProperty(new Object(), toggler);

        // It's showtime folks!
        control_frame.setVisible(true);
        controlled_window.setVisible(true);
    }


    /**
     * A somewhat more complex example. In this case the resource and the
     * controller live in the same panel. The "remove" button allows you to
     * remove the resource--in this case a colored label--which then becomes
     * garbage collectible.
     *
     * The main functional difference between this example and the previous
     * one is in the way the listener is kept alive by the thing it's affecting.
     * In the previous example the hard reference was created by explicitly
     * adding the listener as a client property of an object within the window
     * it affects. In this case that reference is implicit in the fact that
     * the listener lives as a member variable within the class it affects.
     * Either way works fine. Just choose from these or other ways of making
     * the association you need depending upon what's most natural in each case
     * where you want to use a WeakProxyReference.
     */
    private static void buttonSelectionTest()  {
        // here's an inner class that represents a JLabel with toggleable color.
        // the toggle() method switches between two different background colors.
        class ColoredLabel extends JLabel {
            private boolean green = true; // current background color
            // NOTE: The line below creates and stores the action listener
            // inside the ColoredLabel class. The fact that it is stored here
            // is what keeps the listener from being garbage collected too soon.
            // It's essentially a hard reference to that listener that won't
            // be garbage collected until its containing ColoredLabel instance
            // is also collectible. That happens as soon as the label is removed
            // from it's containing panel by clicking the "remover" button.
            private ActionListener toggler = new ActionListener() {
                public void actionPerformed(ActionEvent ae) { 
                    toggle();
                }
            };
            public ActionListener getController()  {
                return toggler;
            }    
            private ColoredLabel(String title) {
                super(title);
                setFont(new Font("Courier", Font.BOLD, 30));
                toggle(); // to set original background color
            }
            private void toggle() {
                System.out.println("colored window being toggled");
                green = !green;
                setForeground(green ? Color.green : Color.blue);
                repaint();
                System.gc();
            }
        }
        final ColoredLabel text = new ColoredLabel("Resource");
        JButton control_button = new JButton("Toggle text field");
        JFrame control_frame = new JFrame("WeakProxyReference Test");
        final JButton remover = new JButton("Remove Resources");
        final JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        remover.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                panel.remove(text);
                panel.remove(remover);
                //text label and remover button are now garbage collectable
                panel.repaint();
            }
        });
        control_frame.getContentPane().add(panel);
        control_button.setAlignmentX(0.5f);
        panel.add(control_button);
        text.setAlignmentX(0.5f);
        panel.add(text);
        remover.setAlignmentX(0.5f);
        panel.add(remover);
        JTextArea instructions = new JTextArea(
            "The toggle button controls the color of the 'Resource' label. "+
            "That label is acting as some expensive resource that you don't "+
            "want kept alive just because by the control button has a "+
            "listener which affects it. The 'remove' button removes both "+
            "the resource label and the remove button itself, both of which "+
            "then become garbage collectable. "+
            "To see the console message that the resource has indeed been "+
            "garbage collected, you need to compile this code with the DEBUG "+
            "flag set to 'true' and you may need to maximize the window,  "+
            "restore it to normal, and hit the toggle button once more "+
            "before you see that the garbage collecter has done it's thing.");
        instructions.setLineWrap(true);
        instructions.setWrapStyleWord(true);
        panel.add(instructions);
        control_frame.setLocation(new Point(100, 100));
        control_frame.setSize(new Dimension(300, 300));
        control_frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //Rather than calling
        //control_button.addActionListener(text.getController())
        //like we normally would, here's the magic call to set up
        //the weak proxy reference to the button listener
        WeakProxyReference.create(control_button, text.getController(), 
            ActionListener.class);
        control_frame.setVisible(true);
    }

}