/* * Copyright (c) 2006,2009 Declarative Engineering LLC. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Declarative Engineering LLC * verson 1 which accompanies this distribution, and is available at * http://declarativeengineering.com/legal/DE_Developer_License_v1.txt */ package com.foundation.view; import com.common.util.IHashSet; import com.common.util.IIterator; import com.common.util.LiteHashSet; import com.foundation.event.EventSupport; import com.foundation.event.IEventEmitter; import com.foundation.event.IEventHandler; import com.foundation.metadata.Attribute; import com.foundation.util.IManagedCollection; import com.foundation.view.EventAssociation; import com.foundation.view.IAssociationHandler; import com.foundation.view.IEventAssociation; import com.foundation.view.IEventAssociationChangeListener; import com.foundation.view.IValueHolderListener; import com.foundation.view.MultiAssociationContainer.RowDataContainer; /* * Models an association that results in one value for each row value given to it. */ public class MultiAssociation extends Association implements IEventAssociationChangeListener, IValueHolderListener { /** The set of events that the association registers for when the input changes. If any of these events are triggered it indicates that this association's value has changed. */ private EventAssociation[] events = null; /** Tracks the row data instances created by the multi-association. */ private IHashSet rowDataSet = null; /** Whether the association produces a target result. If this is false then the result will be feed back into the pool of associations in the container. */ private boolean isTarget = false; /** The attribute to be used to access decorations for the association. If this is null then the object its self will have the decorations. */ private Attribute decoratedAttribute; /** Whether decorations should be accessed for the association. */ private boolean useDecorations; /** The optional fixed value. This will only be used if the association identifier is -2. */ private Object fixedValue; /** * Makes up a doublely linked list where the start of the list is held by the association container. * Each row data in the list represents one step in the chain of objects navigated to get to the final target result. */ public static class RowData extends MultiAssociationContainer.RowData implements IEventHandler { /** The row that last generated the result. */ private Object row = null; /** The last known result for this association. */ private Object result = null; /** The next association in the chain. This should always be null if this is a target association. */ private MultiAssociation.RowData next = null; /** The previous association in the chain. This should always be null if this is a target association. */ private MultiAssociation.RowData previous = null; /** The support object that tracks events registered via the AssociationNodes. This will only be non-null if association attribute and nodes were provided. */ private EventSupport nodeEventSupport = null; /** The container for the row that this row data models. */ private MultiAssociationContainer.RowDataContainer rowDataContainer = null; /** * RowData constructor. * @param association The association that created the row data. * @param row The row object that initialized the row data and generated the result. * @param rowDataContainer The container that this row data exists in. * @param result The result for the row. */ public RowData(MultiAssociation association, Object row, MultiAssociationContainer.RowDataContainer rowDataContainer, Object result) { super(association); this.row = row; this.rowDataContainer = rowDataContainer; this.result = result; }//RowData()// /** * Gets the latest result for the association row. * @return The association row's current result. */ public Object getResult() { return result; }//getResult()// /** * Gets the next association in the chain. * @return The next association in the chain. */ public MultiAssociation.RowData getNext() { return next; }//getNext()// /** * Sets the next association in the chain. * @param next The next association in the chain. */ public void setNext(MultiAssociation.RowData next) { this.next = next; }//setNext()// /** * Gets the previous association in the chain. * @return The previous association in the chain. */ public MultiAssociation.RowData getPrevious() { return previous; }//getPrevious()// /** * Sets the previous association in the chain. * @param previous The previous association in the chain. */ public void setPrevious(MultiAssociation.RowData previous) { this.previous = previous; }//setPrevious()// /** * Gets the row data container. * @return The container containing this row data. */ public MultiAssociationContainer.RowDataContainer getContainer() { return rowDataContainer; }//getContainer()// /** * Gets the row that this row data describes. * @return The row object which was input for this row data. */ public Object getRow() { return row; }//getRow()// /* (non-Javadoc) * @see com.foundation.event.IEventHandler#evaluate(com.foundation.event.IEventEmitter, int, java.lang.Object[], int) */ public void evaluate(IEventEmitter eventEmitter, int eventNumber, Object[] eventParameters, int flags) { //Called when an event, registered on the row object that generated the result, is fired.// ((MultiAssociation) getAssociation()).updateAssociation(this, getAssociation().isTargetAssociation() ? flags : 0); }//evaluate()// /* (non-Javadoc) * @see com.foundation.event.IHandler#evaluate(int, java.lang.Object[], int) */ public void evaluate(int eventNumber, Object[] eventParameters, int flags) { //Never called.// }//evaluate()// }//RowData// /** * MultiAssociation constructor. * @param rowType The type of row the association applies to. In the case of a root association in a single association this is the type held by the value holder. * @param fixedValue The value to return as the result. */ public MultiAssociation(Class rowType, Object fixedValue, boolean isTarget) { this(rowType, -2, null, false, null, null, null, null, isTarget, null, false, fixedValue); }//MultiAssociation()// /** * MultiAssociation constructor. * @param rowType The type of row the association applies to. In the case of a root association in a single association this is the type held by the value holder. * @param associationNumber The association number which will be passed to the handler when the assocation is invoked. This number allows fast indexing of the methods that the handler handles. This value may be -1 if the input value should be used as the output value for the association (short circuiting). * @param handler The object that handles the method invocations to get and set the value. * @param invertLogic Whether the result of the association will be inverted. This is ignored unless the result is a Boolean value. * @param attributes The attributes to listen to and whose values are passed to the nodes. This may be null. * @param nodes The nodes of the arbitrary tree of listeners for value change events for this association's result value. This may be null. * @param children The child associations in search order. The children will be searched for the first match for this association's result. This should always be null for target associations, and should never be null for non-target associations. * @param events The non-null array of events that will be used to listen to the supplied row object for changes in the result. * @param valueHolderName The name of the optional value holder whose value holds the getter and setter methods used by the association. If no value holder is defined then the row object passed to the association must define the getter and setter methods. */ public MultiAssociation(Class rowType, int associationNumber, IAssociationHandler handler, boolean invertLogic, AssociationAttribute[] attributes, AssociationNode[] nodes, EventAssociation[] events, String valueHolderName, boolean isTarget) { this(rowType, associationNumber, handler, invertLogic, attributes, nodes, events, valueHolderName, isTarget, null, false, null); if(events == null) { throw new IllegalArgumentException("The events parameter may not be null."); }//if// }//MultiAssociation()// /** * MultiAssociation constructor. * @param rowType The type of row the association applies to. In the case of a root association in a single association this is the type held by the value holder. * @param associationNumber The association number which will be passed to the handler when the assocation is invoked. This number allows fast indexing of the methods that the handler handles. This value may be -1 if the input value should be used as the output value for the association (short circuiting). * @param handler The object that handles the method invocations to get and set the value. * @param invertLogic Whether the result of the association will be inverted. This is ignored unless the result is a Boolean value. * @param attributes The attributes to listen to and whose values are passed to the nodes. This may be null. * @param nodes The nodes of the arbitrary tree of listeners for value change events for this association's result value. This may be null. * @param children The child associations in search order. The children will be searched for the first match for this association's result. This should always be null for target associations, and should never be null for non-target associations. * @param events The non-null array of events that will be used to listen to the supplied row object for changes in the result. * @param valueHolderName The name of the optional value holder whose value holds the getter and setter methods used by the association. If no value holder is defined then the row object passed to the association must define the getter and setter methods. * @param decoratedAttribute The attribute that the decorations will be attached to, or null if the object its self will be decorated. */ public MultiAssociation(Class rowType, int associationNumber, IAssociationHandler handler, boolean invertLogic, AssociationAttribute[] attributes, AssociationNode[] nodes, EventAssociation[] events, String valueHolderName, boolean isTarget, Attribute decoratedAttribute) { this(rowType, associationNumber, handler, invertLogic, attributes, nodes, events, valueHolderName, isTarget, decoratedAttribute, true, null); if(events == null) { throw new IllegalArgumentException("The events parameter may not be null."); }//if// }//MultiAssociation()// /** * MultiAssociation constructor. * @param rowType The type of row the association applies to. In the case of a root association in a single association this is the type held by the value holder. * @param associationNumber The association number which will be passed to the handler when the assocation is invoked. This number allows fast indexing of the methods that the handler handles. This value may be -1 if the input value should be used as the output value for the association (short circuiting). * @param handler The object that handles the method invocations to get and set the value. * @param invertLogic Whether the result of the association will be inverted. This is ignored unless the result is a Boolean value. * @param attributes The attributes to listen to and whose values are passed to the nodes. This may be null. * @param nodes The nodes of the arbitrary tree of listeners for value change events for this association's result value. This may be null. * @param children The child associations in search order. The children will be searched for the first match for this association's result. This should always be null for target associations, and should never be null for non-target associations. * @param events The non-null array of events that will be used to listen to the supplied row object for changes in the result. * @param valueHolderName The name of the optional value holder whose value holds the getter and setter methods used by the association. If no value holder is defined then the row object passed to the association must define the getter and setter methods. * @param decoratedAttribute The attribute that the decorations will be attached to, or null if the object its self will be decorated. * @param useDecorations Whether the association should link to the decorations mappings. This is required because the decorated attribute may be null and the association may still link to the decorations. */ protected MultiAssociation(Class rowType, int associationNumber, IAssociationHandler handler, boolean invertLogic, AssociationAttribute[] attributes, AssociationNode[] nodes, EventAssociation[] events, String valueHolderName, boolean isTarget, Attribute decoratedAttribute, boolean useDecorations, Object fixedValue) { super(rowType, associationNumber, handler, invertLogic, attributes, nodes, valueHolderName); this.fixedValue = fixedValue; this.isTarget = isTarget; this.events = events; this.useDecorations = useDecorations; this.decoratedAttribute = decoratedAttribute; if(events != null) { for(int index = 0; index < events.length; index++) { events[index].setChangeListener(this); }//for// }//if// }//MultiAssociation()// /** * Gets the attribute whose decorations are referenced by this association. * @return The decorated attribute for this association, or null if the row object its self will be decorated. */ public Attribute getDecoratedAttribute() { return decoratedAttribute; }//getDecoratedAttribute()// /** * Gets whether the association should listen for decorations. * @return Whether the association is linked to the model for accessing decorations. */ public boolean useDecorations() { return useDecorations; }//useDecorations()// /** * Determines whether this is a target association meaning that its result is the value assigned to the initial association input. * @return Whether this association is a target and the result is not placed as input to another association. */ public boolean isTargetAssociation() { return isTarget; }//isTargetAssociation()// /** * Gets the current result of the association from the model. * @param row The row object that is the input to the association. * @return The result of the association. */ protected Object getResult(Object row) { Object result; int associationNumber = getAssociationNumber(); if(getIsValueHolderAssociated()) { if(associationNumber == -1) { result = getValueHolder().getValue(); }//if// else if(associationNumber == -2) { result = fixedValue; }//else if// else { if(oneArray == null) { oneArray = new Object[1]; }//if// oneArray[0] = row; result = getHandler().invokeMethod(associationNumber, getValueHolder().getValue(), oneArray, IAssociationHandler.INVOKE_GETTER_METHOD_FLAG); oneArray[0] = null; }//if// }//if// else { if(associationNumber == -1) { result = row; }//if// else if(associationNumber == -2) { result = fixedValue; }//else if// else { result = getHandler().invokeMethod(associationNumber, row, null, IAssociationHandler.INVOKE_GETTER_METHOD_FLAG); }//else// }//else// if(isTargetAssociation()) { result = convertValueToControl(result); }//if// return result; }//getResult()// /** * Gets the original value for the association. * @return The value that the user has replaced via the view. */ public Object getOriginalResult(Object row) { Object result = null; int associationNumber = getAssociationNumber(); //Ignore non-target associations, short-circuit and fixed value setups.// if(isTargetAssociation() && associationNumber != -1 && associationNumber != -2) { if(getIsValueHolderAssociated()) { if(oneArray == null) { oneArray = new Object[1]; }//if// oneArray[0] = row; result = getHandler().invokeMethod(associationNumber, getValueHolder().getValue(), oneArray, IAssociationHandler.INVOKE_ORIGINAL_VALUE_GETTER_METHOD_FLAG); oneArray[0] = null; }//if// else { result = getHandler().invokeMethod(associationNumber, row, null, IAssociationHandler.INVOKE_ORIGINAL_VALUE_GETTER_METHOD_FLAG); }//else// result = convertValueToControl(result); }//if// return result; }//getOriginalResult()// /** * Sets the new result of the association into the model. * @param rowData The row object that is the input to the association. * @param result The new result which will be placed in the model. */ public void setResult(MultiAssociation.RowData rowData, Object result) { int associationNumber = getAssociationNumber(); //Ignore the short-circuit and fixed value setups.// if(associationNumber != -1 && associationNumber != -2) { result = convertValueToModel(result); if(getIsValueHolderAssociated()) { if(twoArray == null) { twoArray = new Object[2]; }//if// twoArray[0] = rowData.getRow(); twoArray[1] = result; getHandler().invokeMethod(associationNumber, getValueHolder().getValue(), twoArray, IAssociationHandler.INVOKE_SETTER_METHOD_FLAG); twoArray[0] = null; twoArray[1] = null; }//if// else { if(oneArray == null) { oneArray = new Object[1]; }//if// oneArray[0] = result; getHandler().invokeMethod(associationNumber, rowData.getRow(), oneArray, IAssociationHandler.INVOKE_SETTER_METHOD_FLAG); oneArray[0] = null; }//else// }//if// }//setResult()// /** * Called by the event handlers to update the association state when the result may have altered. * @param rowData The row data for the row being updated. * @param eventFlags The event flags that for the event that triggered the update. This will only be non-zero if an event triggered the call and the event was either an original value changed or cleared event. */ protected void updateAssociation(MultiAssociation.RowData rowData, int eventFlags) { if(EventSupport.isOriginalValueChanged(eventFlags) || EventSupport.isOriginalValueCleared(eventFlags)) { ((MultiAssociationContainer) getAssociationContainer()).updateAssociations(rowData, null, eventFlags); }//if// else { Object row = rowData.row; Object newResult = getResult(row); if(newResult != rowData.result) { Object oldResult = rowData.result; unregisterListeners(row, rowData); rowData.result = newResult; registerListeners(row, rowData); ((MultiAssociationContainer) getAssociationContainer()).updateAssociations(rowData, oldResult, eventFlags); }//if// else { //Always re-register all the extended listeners since they cannot handle attribute changes on their own.// unregisterListeners(row, rowData); registerListeners(row, rowData); }//else// }//else// }//updateAssociation()// /** * Registers a value with the association for use by a single association. * @param row The next value in the chain of associations for the single association. * @param rowData The array of row data used by the associations to store depth specific data. This association may only access the index at its depth. */ public MultiAssociation.RowData register(Object row, MultiAssociationContainer.RowDataContainer rowDataContainer) { //TODO: We could optimize this for the case where getAssociationNumber() == -1 or -2 (indicating that the row is the result [-1], or there is a fixed value for the result [-2]). return register(row, rowDataContainer, getResult(row)); }//register()// /** * Registers a value with the association for use by a single association. * @param row The next value in the chain of associations for the single association. * @param rowDataContainer The container row data used by the associations to store data. * @param result The result for the row. */ protected MultiAssociation.RowData register(Object row, RowDataContainer rowDataContainer, Object result) { RowData rowData = new RowData(this, row, rowDataContainer, result); if(events != null) { //Register all events.// for(int index = 0; index < events.length; index++) { EventAssociation eventAssociation = (EventAssociation) events[index]; if(eventAssociation.getIsValueHolderAssociated()) { //Register for the value holder's value changing and for the event on the held value (uses the value holder's event support).// eventAssociation.register(); }//if// else if(row instanceof IEventEmitter) { //Register with the event on the row object using the resource association's event support.// rowDataContainer.getEventSupport().register((IEventEmitter) row, eventAssociation.getEventNumber(), rowData, true); }//else// }//for// }//if// //Register the listener with the value holder.// if(getIsValueHolderAssociated()) { //Note: The value holder maintains a counter so we don't have to. This can be called multiple times without any negative effects.// getValueHolder().registerListener(this); }//if// if(rowDataSet == null) { rowDataSet = new LiteHashSet(100, LiteHashSet.DEFAULT_LOAD_FACTOR, LiteHashSet.DEFAULT_COMPARATOR, LiteHashSet.STYLE_COUNT_DUPLICATES); }//if// rowDataSet.add(rowData); registerListeners(row, rowData); return rowData; }//register()// /** * Registers listeners with the result. * @param row The association's row to register all extended listeners with. * @param rowData The data for the row that is registering. */ protected void registerListeners(Object row, RowData rowData) { AssociationAttribute[] attributes = getAttributes(); if(attributes != null) { IHashSet trackedValues = new LiteHashSet(100); if(rowData.nodeEventSupport == null) { rowData.nodeEventSupport = new EventSupport(null); }//if// //TODO: It would be more efficient in some cases if we detected duplicate results for different row datas and had the events go to all affected row data's while only registering once. //This idea could turn out to be less efficient than the current design since it would require a more complex data structure. for(int index = 0; index < attributes.length; index++) { Object attributeValue = attributes[index].getValue(row); if(!trackedValues.containsValue(attributeValue)) { trackedValues.add(attributeValue); //TODO: Should we support other collections? If so we would need to be notified when there are changes... if(attributeValue instanceof IManagedCollection) { IIterator iterator = ((IManagedCollection) attributeValue).iterator(); //Register for changes in the collected values.// while(iterator.hasNext()) { registerListeners(rowData.nodeEventSupport, trackedValues, rowData, iterator.next()); }//while// //Register for changes in the collection.// rowData.nodeEventSupport.register((IManagedCollection) attributeValue, IManagedCollection.EVENT, rowData, true); }//if// else { //Register for changes in the attribute value.// registerListeners(rowData.nodeEventSupport, trackedValues, rowData, attributeValue); }//else// }//if// }//for// }//if// }//registerListeners()// /** * Unregisters the row. * @param rowData The row data that was generated by the preceeding registeration. */ public void unregister(MultiAssociation.RowData rowData) { boolean removeFromRowDataSet = false; if(events != null) { //Unregister all events.// for(int index = 0; index < events.length; index++) { EventAssociation eventAssociation = (EventAssociation) events[index]; if(eventAssociation.getIsValueHolderAssociated()) { //Register for the value holder's value changing and for the event on the held value (uses the value holder's event support).// eventAssociation.unregister(); removeFromRowDataSet = true; }//if// else if(rowData.getRow() instanceof IEventEmitter) { //Register with the event on the row object using the resource association's event support.// rowData.getContainer().getEventSupport().unregister((IEventEmitter) rowData.getRow(), eventAssociation.getEventNumber(), rowData); }//else// }//for// }//if// //Unregister the listener with the value holder.// if(getIsValueHolderAssociated()) { //Note: The value holder uses a counter so we don't have to.// getValueHolder().unregisterListener(this); removeFromRowDataSet = true; }//if// if(removeFromRowDataSet) { rowDataSet.remove(rowData); }//if// unregisterListeners(rowData.getRow(), rowData); }//unregister()// /** * Unregisters listeners with the result. * @param row The association's result to unregister all extended listeners with. * @param rowData The data for the row that is unregistering. */ protected void unregisterListeners(Object row, RowData rowData) { AssociationAttribute[] attributes = getAttributes(); if(attributes != null) { rowData.nodeEventSupport.unregisterAll(); }//if// }//unregisterListeners()// /* (non-Javadoc) * @see com.foundation.view.IEventAssociationChangeListener#onEventFired(com.foundation.view.IEventAssociation, java.lang.Object[]) */ public void onEventFired(IEventAssociation eventAssociation, Object[] eventArguments) { //Called when an event is fired from a value holder. In this case all rows are affected by the event.// if(rowDataSet != null) { for(IIterator iterator = rowDataSet.iterator(); iterator.hasNext();) { updateAssociation((RowData) iterator.next(), 0); }//for// }//if// }//onEventFired()// /* (non-Javadoc) * @see com.foundation.view.IValueHolderListener#heldValueChanged() */ public void heldValueChanged() { //Called when a value holder, that returned the result given the row value, has changed its value.// IIterator iterator = rowDataSet.iterator(); while(iterator.hasNext()) { updateAssociation((RowData) iterator.next(), 0); }//while// }//heldValueChanged()// /* (non-Javadoc) * @see com.foundation.view.Association#initialize(com.foundation.view.AssociationContainer) */ public void initialize(AssociationContainer associationContainer) { super.initialize(associationContainer); }//initialize()// /* (non-Javadoc) * @see com.foundation.view.Association#release() */ public void release() { super.release(); }//release()// }//MultiAssociation//