import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.WeakHashMap;

/**
 * Hilfsklasse für das Handling von anonymen Listener-Objekten
 * verschiedener Art, die trotz ihrer Referenz zum Elternobjekt
 * entfernbar sein sollen.
 * 
 * Problem: Es gibt ein langlebiges Model-Objekt. Eine GUI,
 * die dieses Model bearbeiten soll, hängt ein PropertyChangeEvent
 * in das Model ein, welches ein dazugehöriges Eingabefeld
 * aktualisieren soll.
 * 
 * Da dieser, von Model nun stark referenzierte Listener eine
 * (wenn auch u.U. implizite) Referenz zur GUI-Komponente besitzt,
 * kann diese nicht vom Garbage-Collector entfernt werden, solange 
 * das Model existiert.
 * 
 * Auf diese Weise kann es in einer langlebigen Anwendung dazu kommen,
 * dass sich die Anzahl der unsichtbaren GUI-Komponenten und Property-
 * ChangeListener beständig erhöht und eine Menge Performance kostet.
 * 
 * Eine Lösung ist es, eine schwache (weak) Referenz zwischen Modelobjekt
 * und Listener zu schaffen. Anonyme Listener, die sonst niemandem gehören,
 * würden dadurch jedoch sofort weggeräumt werden. Sie müssen also durch
 * jemanden referenziert werden, bis das Elternobjekt freigegeben werden
 * kann.
 * 
 * Genau das leistet diese Klasse. Ihre Anwendung ist analog zur Klasse
 * PropertyChangeSupport: Das Elternobjekt definiert eine Instanzvariable
 * mit dem WeakListenerSupport. Alle in das Model eingehängten Listener
 * werden durch die Methoden dieses Objekts gekappselt (und vom Objekt
 * in einer Liste abgelegt, so dass sie vor Änderungen geschützt sind).
 * 
 * Die Listener werden durch eine weitere Listener-Objekt gekappselt,
 * welches sie mittels WeakReference verknüpft. Sobald ein Event ausgelöst
 * wird, schaut der WeakListener nach, ob der Ziellistener noch
 * existiert. Falls ja, wird das Event weitergereicht. Falls nein wird
 * durch den Aufruf der jeweiligen Standardmethode einmalig versucht,
 * den WeakListener aus dem Model zu entfernen. Selbst wenn das nicht gelingt
 * reduziert sich der dadurch entstehende Memory-Leak jedoch gewaltig!
 * 
 * Ein weiteres nettes Feature, was man auf diese Weise enthält, ist
 * die Möglichkeit, alle Listener gleichzeitig durch den Aufruf von
 * "removeAll(Object)" aus einem Modelobjekt zu entfernen.
 * 
 * Anwendungsbeispiel:
 * 
 * <code>
 * public class MyPanel extends JPanel {
 *     private final WeakListenerSupport wls;
 *     private final JTextField textfield;
 *     private final Model model;
 *     
 *     public MyPanel(Model model) {
 *         this.model = model;
 *         this.wls = new WeakListenerSupport();
 *         this.textfield = new JTextField(model.getValue());
 *         this.model.addPropertyChangeListener("value", wls.propertyChange(new PropertyChangeListener() {
 *             @Override
 *             public void propertyChange(PropertyChangeEvent event) {
 *                 textfield.setText(model.getValue());
 *             }
 *         }, this.model));
 *         add(this.textfield);
 *     }
 * }
 * </code>
 * 
 * @author Roland Tapken (java@rt.tasmiro.de)
 * @license Public Domain
 */
public class WeakListenerSupport {
	
	/**
	 * Schützt die Listener solange vor dem GarbageCollector, bis
	 * der Besitzter dieses Objekts ebenfalls weggeräumt werden kann.
	 * Jeder Listener, für den ein WeakListener erzeugt wird,
	 * muss in diese Liste eingefügt werden.
	 */
	private ArrayList<Object> listeners = new ArrayList<Object>();
	
	/**
	 * Ermöglicht die Entfernung aller Listener
	 * (eines bestimmten Model-Objekts) auf einen Schlag.
	 * Es werden nur die Keys verwendet, im Objekt steht
	 * immer null.
	 */
	private WeakHashMap<WeakListener<?>, Object> weakListeners = new WeakHashMap<WeakListener<?>, Object>();

	/**
	 * Entfernt alle Listeners und versetzt dieses Objekt
	 * quasi in den Ausgangszustand zurück.
	 */
	public void removeAll() {
		for (WeakListener<?> l : weakListeners.keySet()) {
			if (l != null) {
				l.unlink();
			}
		}
		listeners.clear();
		weakListeners.clear();
	}
	
	/**
	 * Entfernt alle Listeners, die in einem bestimmten
	 * Objekt eingehängt sind. Hinweis: Es wird nach
	 * exakt dem selben Model gesucht, es wird nicht
	 * equals() verwendet.
	 */
	public void removeAll(Object model) {
		for (WeakListener<?> l : new ArrayList<WeakListener<?>>(weakListeners.keySet())) {
			if (l != null && l.getModel() == model) {
				listeners.remove(l.getListener());
				weakListeners.remove(l);
				l.unlink();
			}
		}
	}
	
	
	public WeakPropertyChangeListener propertyChange(PropertyChangeListener listener, Object model, String removeMethodName) {
		return new WeakPropertyChangeListener(this, listener, model, removeMethodName);
	}

	public WeakPropertyChangeListener propertyChange(PropertyChangeListener listener, Object model) {
		return propertyChange(listener, model, null);
	}

	public WeakPropertyChangeListener propertyChange(PropertyChangeListener listener) {
		return propertyChange(listener, null, null);
	}

	/**
	 * Container-Klasse für WeakListeners. Sie stellt
	 * einige Grundfunktionen bereit. Das Template trägt
	 * den Namen der Klasse des gekappselten Listener-Objekts.
	 */
	private abstract static class WeakListener<T> {
		private WeakReference<Object> model;
		private WeakReference<T> listener;
		private Class<?> listenerClass;
		private String removeListenerMethod; 
		
		private WeakListener(WeakListenerSupport wls, T listener, Object model, String removeListenerMethod) {
			this.model = new WeakReference<Object>(model);
			this.listener = new WeakReference<T>(listener);
			this.listenerClass = listener == null ? null : listener.getClass();
			this.removeListenerMethod = removeListenerMethod == null ? "" : removeListenerMethod;
			
			// Register this at the support object
			wls.listeners.add(listener);
			wls.weakListeners.put(this, null);
		}
		
		public final Object getModel() {
			Object model = this.model.get();
			return model == null ? null : model;
		}
		
		public final T getListener() {
			T listener = this.listener.get();
			return listener == null ? null : listener;
		}
		
		/**
		 * Diese Methode versucht, den aktuellen Listener 
		 * aus dem Zielobjekt zu entfernen.
		 */
		protected void unlink() {
			if (this.model != null)  {
				try {
					Object model = getModel();
					if (model != null && !removeListenerMethod.isEmpty()) {						
						try {
							Method m = model.getClass().getMethod(removeListenerMethod, listenerClass);
							m.invoke(model, this);
						} catch (Exception e) {
						}
					}
				} finally {
					// Free as much as we have
					this.model = null;
					this.listener = null;
					this.listenerClass = null;
					this.removeListenerMethod = null;
				}
			}
		}
	}
	
	private static class WeakPropertyChangeListener extends WeakListener<PropertyChangeListener> implements PropertyChangeListener {
		private WeakPropertyChangeListener(WeakListenerSupport wls, PropertyChangeListener listener, Object model, String removeMethodName) {
			super(wls, listener, model, removeMethodName);
		}

		@Override
		public void propertyChange(PropertyChangeEvent evt) {
			PropertyChangeListener listener = getListener();
			if (listener != null) {
				listener.propertyChange(evt);
			} else {
				unlink();
			}
		}
	}
}

