/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* GemGraphArgumentManager.java
* Creation date: Jun 22, 2004.
* By: Edward Lam
*/
package org.openquark.gems.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.openquark.cal.compiler.CALSourceGenerator;
import org.openquark.cal.compiler.CompositionNode;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.GemGraph.InputCollectMode;
import org.openquark.gems.client.GemGraph.TraversalScope;
import org.openquark.util.Pair;
/**
* This class is responsible for managing argument targets and retargeting inputs in response to gem model events.
* @author Edward Lam
*/
public class GemGraphArgumentManager {
/** The target collector. Arguments will often be retargeted at this collector gem. */
private final CollectorGem targetCollector;
/** A flag to disable argument updating. */
private boolean disableArgumentUpdating = false;
/**
* Constructor for an GemGraphArgumentManager.
* @param targetCollector the target collector.
*/
GemGraphArgumentManager(CollectorGem targetCollector) {
this.targetCollector = targetCollector;
}
/**
* Sets the disableArgumentUpdating flag.
* This causes automatic argument updating by the retargeter to be disabled.
* @param newValue The new value. If false, argument updating is enabled. However, affected reflectors will not have been updated.
* @return the old value.
*/
boolean setArgumentUpdatingDisabled(boolean newValue) {
boolean oldSetting = this.disableArgumentUpdating;
this.disableArgumentUpdating = newValue;
return oldSetting;
}
/**
* Retarget arguments after a connection has taken place.
* @param conn the connection which was made.
*/
public void retargetForConnect(Connection conn) {
if (disableArgumentUpdating) {
return;
}
PartInput destInput = conn.getDestination();
Gem destGem = destInput.getGem();
// Get the input argument which is disappearing. Note the affected collector.
CollectorGem replacedArgumentTarget = GemGraph.getInputArgumentTarget(destInput);
// Make sure inputs from the connected subtree make sense.
reconcileArgumentTargetsOnConnect(conn);
// Update the corresponding collectors.
Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
// Add the collector targeted by the input being connected.
if (replacedArgumentTarget != null) {
affectedCollectorSet.add(replacedArgumentTarget);
}
// Find collectors targeted by free inputs in the descendant gem forest
// (when considering the gem graph after connection)
List<PartInput> freeInputsInDescendantForestList =
GemGraph.obtainUnboundDescendantInputs(destGem, TraversalScope.FOREST, InputCollectMode.UNBURNT_ONLY);
for (final PartInput partInput : freeInputsInDescendantForestList) {
CollectorGem inputArgumentTarget = GemGraph.getInputArgumentTarget(partInput);
if (inputArgumentTarget != null) {
affectedCollectorSet.add(inputArgumentTarget);
}
}
// Now update the gem graph for affected collectors.
updateForArgumentChange(affectedCollectorSet);
}
/**
* Retarget arguments after a disconnection has taken place.
* @param conn the connection which was disconnected.
*/
public void retargetForDisconnect(Connection conn) {
if (disableArgumentUpdating) {
return;
}
// Make sure inputs from the disconnected subtree make sense.
Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>(reconcileArgumentTargetsOnDisconnect(conn));
{
PartInput freedInput = conn.getDestination();
Gem.PartOutput disconnectedOutput = conn.getSource();
// (HACK!?) Temporarily re-bind a connection so that argument target resolution can take place for the connected tree.
// Collectors targeted by arguments on trees rooted at collectors at inner scopes of the current tree will be affected.
freedInput.bindConnection(conn);
disconnectedOutput.bindConnection(conn);
// Find collectors targeted by free inputs in the descendant gem forest
// (when considering the gem graph after connection)
List<PartInput> freeInputsInDescendantForestList =
GemGraph.obtainUnboundDescendantInputs(freedInput.getGem(), TraversalScope.FOREST, InputCollectMode.UNBURNT_ONLY);
for (final PartInput partInput : freeInputsInDescendantForestList) {
CollectorGem inputArgumentTarget = GemGraph.getInputArgumentTarget(partInput);
if (inputArgumentTarget != null) {
affectedCollectorSet.add(inputArgumentTarget);
}
}
// Unbind the connection.
freedInput.bindConnection(null);
disconnectedOutput.bindConnection(null);
}
// Update the corresponding collectors.
updateForArgumentChange(affectedCollectorSet);
}
/**
* Retarget an input argument from one collector to another.
* @param collectorArgument the argument to retarget.
* @param newTarget the collector to which the input will be retargeted.
* @param addIndex the index at which the retargeted argument will be placed, or -1 to add to the end.
* @return the old argument index, or -1 if there was no old target.
*/
public int retargetInputArgument(PartInput collectorArgument, CollectorGem newTarget, int addIndex) {
CollectorGem oldTarget = GemGraph.getInputArgumentTarget(collectorArgument);
Set<CollectorGem> affectedCollectors = new HashSet<CollectorGem>();
// Remove the argument from its old target if any.
int oldArgIndex = -1;
if (oldTarget != null) {
oldArgIndex = oldTarget.getTargetArguments().indexOf(collectorArgument);
oldTarget.removeArgument(collectorArgument);
affectedCollectors.add(oldTarget);
}
// Add the argument to its new target.
if (addIndex < 0) {
newTarget.addArgument(collectorArgument);
} else {
newTarget.addArguments(addIndex, Collections.singleton(collectorArgument));
}
affectedCollectors.add(newTarget);
// Update collectors and reflectors..
updateForArgumentChange(affectedCollectors);
return oldArgIndex;
}
/**
* Retarget the inputs on the given gem for a definition change.
* @param changedGem the gem whose definition changed.
* @param oldInputs the inputs from before the change.
*/
public void retargetArgumentsForDefinitionChange(Gem changedGem, PartInput[] oldInputs) {
if (disableArgumentUpdating) {
return;
}
Set<CollectorGem> affectedCollectorSet = reconcileArgumentsOnDefinitionChange(changedGem, oldInputs);
updateForArgumentChange(affectedCollectorSet);
}
/**
* Ensure that a collector's input is targeted when it is added to the gem graph.
* @param addedCollector the collector which was added.
*/
public void retargetArgumentsOnAdd(CollectorGem addedCollector) {
if (disableArgumentUpdating) {
return;
}
// get the input argument.
PartInput newPartInput = addedCollector.getCollectingPart();
// Add the arg to the target collector, if it's not already targeted.
// TODOEL: this assumes that the target is (or will be) in the graph.
CollectorGem inputTarget = GemGraph.getInputArgumentTarget(newPartInput);
if (inputTarget == null) {
addTargetArgsInSourceOrder(targetCollector, Collections.singleton(newPartInput));
}
// Update its reflected inputs.
updateForArgumentChange(Collections.singleton(addedCollector));
}
/**
* Ensure that a collector's input and targeting inputs are properly (un)targeted when it is removed from the gem graph.
* @param removedCollector the collector which was removed.
* @param retargetCollector the collector to which any arguments targeting the removed collector will be retargeted.
*/
public void retargetArgumentsOnRemove(CollectorGem removedCollector, CollectorGem retargetCollector) {
if (disableArgumentUpdating) {
return;
}
PartInput collectingPart = removedCollector.getCollectingPart();
CollectorGem collectingPartArgTarget = GemGraph.getInputArgumentTarget(collectingPart);
// Remove the arg from its target, if any.
if (collectingPartArgTarget != null) {
collectingPartArgTarget.removeArgument(collectingPart);
}
// Update its reflected inputs.
updateForArgumentChange(Collections.singleton(removedCollector));
// Reassign all other arguments targeting the collector to the retarget collector.
List<PartInput> targetArguments = removedCollector.getTargetArguments();
removedCollector.removeArguments(targetArguments);
retargetCollector.addArguments(targetArguments);
}
/**
* When a connection takes place, call this method to reconcile the arguments on the connected subtree
* to make sure that their targets still make sense.
* The old argument will disappear from its target, and new arguments will appear in appropriate target(s).
* @param conn the connection which was made.
*/
private void reconcileArgumentTargetsOnConnect(Connection conn) {
// Get the arguments on the connecting subtree.
Gem.PartOutput outputPart = conn.getSource();
List<PartInput> connectedInputList = GemGraph.obtainUnboundDescendantInputs(outputPart.getGem(), TraversalScope.TREE, InputCollectMode.UNBURNT_ONLY);
PartInput replacedInputArgument = conn.getDestination();
// Get the input argument which is disappearing. Note the affected collector.
CollectorGem replacedArgumentTarget = GemGraph.getInputArgumentTarget(replacedInputArgument);
// The best collector for the new input arguments to target is whatever collector was targeted by the argument being connected.
// The exception is where any of the new inputs belong to reflectors, in which case the best collector to target is the target itself.
boolean reflectorInputsPresent = false;
for (final PartInput input : connectedInputList) {
if (input.getGem() instanceof ReflectorGem) {
reflectorInputsPresent = true;
break;
}
}
CollectorGem collectorToTarget = reflectorInputsPresent ? targetCollector : GemGraph.getInputArgumentTarget(replacedInputArgument);
// Gather the new args, and affected collectors.
Set<PartInput> newArgSet = new LinkedHashSet<PartInput>();
for (final PartInput newInput: connectedInputList) {
newArgSet.add(newInput);
}
// Try to put the arguments in the right place.
if (collectorToTarget == null) {
// Remove the replaced argument (if any) from its target.
if (replacedArgumentTarget != null) {
replacedArgumentTarget.removeArgument(replacedInputArgument);
}
} else if (replacedArgumentTarget == collectorToTarget) {
// Replace the argument with the new arguments.
replacedArgumentTarget.replaceArgument(replacedInputArgument, newArgSet);
} else {
// Remove the replaced argument (if any) from its target.
if (replacedArgumentTarget != null) {
replacedArgumentTarget.removeArgument(replacedInputArgument);
}
// Retarget any new arguments.
if (!newArgSet.isEmpty()) {
// Get the inputs on the tree when connected.
List<PartInput> joinedInputList = GemGraph.obtainUnboundDescendantInputs(replacedInputArgument.getGem().getRootGem(), TraversalScope.TREE,
InputCollectMode.UNBURNT_ONLY);
// Get the first and last indices of the new inputs in the list.
int firstInputIndex = joinedInputList.indexOf(connectedInputList.get(0)); // if empty, should have been caught by the "if".
int lastInputIndex = firstInputIndex + connectedInputList.size() - 1;
if (firstInputIndex < 0) {
throw new IllegalStateException("Programming error.");
}
// retarget according to the connected tree.
retargetInputArgumentsForTree(joinedInputList, firstInputIndex, lastInputIndex); // TODOEL: this doesn't use "collectorToTarget".
}
}
}
/**
* When a disconnection takes place, call this method to reconcile the arguments on the connected subtree
* to make sure that their targets still make sense.
* The old arguments (on the disconnected tree) will disappear from their targets,
* and the new argument (the connection destination) will appear in an appropriate target.
* @param conn the connection which was broken.
* @return the collectors which were directly affected (ie. had arguments added or removed).
*/
private Set<CollectorGem> reconcileArgumentTargetsOnDisconnect(Connection conn) {
// The set of affected collectors.
// Note that in the body of this method we freely add nulls to this set. Null is removed before returning the set to the caller.
Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
PartInput freedInput = conn.getDestination();
Gem.PartOutput disconnectedOutput = conn.getSource();
// Determine whether the disconnected input was originally orphaned (ie. only present because of the connection) when connected.
// We really only have to check for orphaned reflector inputs, since code gem panels check for orphaned code gem inputs.
boolean wasOrphanedReflectorInput = false;
if (freedInput.getGem() instanceof ReflectorGem) {
PartInput reflectedInput = ((ReflectorGem)freedInput.getGem()).getReflectedInput(freedInput);
if (reflectedInput == null || reflectedInput.isConnected()) {
wasOrphanedReflectorInput = true;
} else {
Gem reflectedInputGem = reflectedInput.getGem();
int reflectedInputNum = reflectedInput.getInputNum();
wasOrphanedReflectorInput = reflectedInputNum >= reflectedInputGem.getNInputs() ||
reflectedInputGem.getInputPart(reflectedInputNum) != reflectedInput;
}
}
// Get the inputs on the disconnecting subtree.
List<PartInput> disconnectedTreeInputList = GemGraph.obtainUnboundDescendantInputs(disconnectedOutput.getGem(), TraversalScope.TREE,
InputCollectMode.UNBURNT_ONLY);
// (HACK!?) Temporarily re-bind a connection so that argument target resolution can take place.
freedInput.bindConnection(conn);
disconnectedOutput.bindConnection(conn);
Map<PartInput, CollectorGem> disconnectedInputToTargetMap = new HashMap<PartInput, CollectorGem>();
for (final PartInput disconnectedTreeInput : disconnectedTreeInputList) {
CollectorGem affectedCollector = GemGraph.getInputArgumentTarget(disconnectedTreeInput);
disconnectedInputToTargetMap.put(disconnectedTreeInput, affectedCollector);
}
// Figure out the collector to target.
CollectorGem collectorToTarget = getCollectorToTarget(disconnectedTreeInputList);
if (collectorToTarget == null) {
collectorToTarget = GemGraph.obtainOutermostCollector(freedInput.getGem());
}
affectedCollectorSet.add(collectorToTarget);
// Unbind the connection.
freedInput.bindConnection(null);
disconnectedOutput.bindConnection(null);
// First the case of an orphaned input (just remove the input from its target).
if (wasOrphanedReflectorInput) {
CollectorGem argumentCollectorTarget = disconnectedInputToTargetMap.get(freedInput);
if (argumentCollectorTarget != null) {
argumentCollectorTarget.removeArgument(freedInput);
affectedCollectorSet.add(argumentCollectorTarget);
}
}
// Iterate over the inputs of the tree which was disconnected, removing them from their target collector.
// Also, if necessary retarget the new free argument if any of the disconnecting inputs targeted the desired target.
boolean needToRetargetFreedArgument = !wasOrphanedReflectorInput;
for (final Map.Entry<PartInput, CollectorGem> mapEntry: disconnectedInputToTargetMap.entrySet()) {
PartInput disconnectedTreeInput = mapEntry.getKey();
CollectorGem affectedCollector = mapEntry.getValue();
if (affectedCollector != null) {
// Retarget the new free argument if this argument targets the target collector.
if (needToRetargetFreedArgument && collectorToTarget != null && collectorToTarget.isTargetedBy(disconnectedTreeInput)) {
collectorToTarget.replaceArgument(disconnectedTreeInput, Collections.singleton(freedInput));
needToRetargetFreedArgument = false;
} else {
affectedCollector.removeArgument(disconnectedTreeInput);
}
affectedCollectorSet.add(affectedCollector);
}
}
// re-target the new input's argument if necessary, and we haven't already.
if (needToRetargetFreedArgument && collectorToTarget != null) {
// Retarget according to inputs from the new input's tree.
Gem inputRoot = freedInput.getGem().getRootGem();
List<PartInput> destinationInputList = GemGraph.obtainUnboundDescendantInputs(inputRoot, TraversalScope.TREE, InputCollectMode.UNBURNT_ONLY);
int newInputIndex = destinationInputList.indexOf(freedInput);
if (newInputIndex < 0) {
throw new IllegalStateException("Programming Error.");
}
CollectorGem collectorTargeted = retargetInputArgumentsForTree(destinationInputList, newInputIndex, newInputIndex);
affectedCollectorSet.add(collectorTargeted);
}
// Remove any nulls from the set (for any args above which didn't have a target).
affectedCollectorSet.remove(null);
return affectedCollectorSet;
}
/**
* Remove inputs from their targets.
* @param inputsToRemove the inputs to remove from their targets.
* @return the collectors from which the inputs were removed.
*/
private static Set<CollectorGem> removeFromTargets(Set<PartInput> inputsToRemove) {
Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
// Now remove from their targets any arguments for inputs which are going away.
for (final PartInput inputToRemove : inputsToRemove) {
CollectorGem targetedCollector = GemGraph.getInputArgumentTarget(inputToRemove);
if (targetedCollector != null) {
affectedCollectorSet.add(targetedCollector);
targetedCollector.removeArgument(inputToRemove);
}
}
return affectedCollectorSet;
}
/**
* Get the inputs on the tree before a gem's inputs changed.
* @param changedGem the gem whose inputs changed.
* @param oldInputs the inputs before the gem changed.
* @return the inputs on the connected gem tree before the change.
*/
private static List<PartInput> getOldUnburntTreeInputList(Gem changedGem, PartInput[] oldInputs) {
// Create a code gem with the right number of inputs.
CodeGem codeGem = new CodeGem(oldInputs.length);
// Replace the changed gem connections with the code gem connections.
Set<Connection> oldConnections = new HashSet<Connection>();
for (int i = 0, nOldInputs = oldInputs.length; i < nOldInputs; i++) {
PartInput oldInput = oldInputs[i];
Connection oldInputConnection = oldInput.getConnection();
if (oldInputConnection != null) {
oldConnections.add(oldInputConnection);
Gem.PartOutput source = oldInputConnection.getSource();
PartInput dest = codeGem.getInputPart(i);
Connection newConnection = new Connection(source, dest);
source.bindConnection(newConnection);
dest.bindConnection(newConnection);
}
}
Connection oldOutputConnection = changedGem.getOutputPart().getConnection();
if (oldOutputConnection != null) {
oldConnections.add(oldOutputConnection);
Gem.PartOutput source = codeGem.getOutputPart();
PartInput dest = oldOutputConnection.getDestination();
Connection newConnection = new Connection(source, dest);
source.bindConnection(newConnection);
dest.bindConnection(newConnection);
}
// Grab the old inputs.
List<PartInput> oldUnburntTreeInputList =
new ArrayList<PartInput>(GemGraph.obtainUnboundDescendantInputs(changedGem.getRootGem(), TraversalScope.TREE, InputCollectMode.UNBURNT_ONLY));
// Replace code gem inputs with old inputs.
int index = 0;
for (final PartInput oldUnburntTreeInput : oldUnburntTreeInputList) {
if (oldUnburntTreeInput.getGem() == codeGem) {
PartInput oldInput = oldInputs[oldUnburntTreeInput.getInputNum()];
oldUnburntTreeInputList.set(index, oldInput);
}
index++;
}
// Revert all the connections.
for (final Connection oldConnection : oldConnections) {
oldConnection.getSource().bindConnection(oldConnection);
oldConnection.getDestination().bindConnection(oldConnection);
}
return oldUnburntTreeInputList;
}
/**
* Retarget the inputs on the given gem for a definition change.
* @param changedGem the gem whose definition changed.
* @param oldInputs the inputs from before the change.
* @return the collectors which were affected by argument targeting changes.
*/
private static Set<CollectorGem> reconcileArgumentsOnDefinitionChange(Gem changedGem, PartInput[] oldInputs) {
// TODOEL: make use of cached input targeting info.
// TODOEL: break this method up into smaller pieces.
// TODOEL: handle better the case where the new inputs are in natural order, but tree inputs are not.
// (eg. if one of the branch's had inputs reorganized).
Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
// Check whether there is a collector to target.
Set<CollectorGem> enclosingCollectorSet = GemGraph.obtainEnclosingCollectors(changedGem);
if (enclosingCollectorSet.isEmpty()) {
return affectedCollectorSet;
}
// Get the inputs on the tree to which the gem is attached.
List<PartInput> unburntTreeInputList = GemGraph.obtainUnboundDescendantInputs(changedGem.getRootGem(), TraversalScope.TREE,
InputCollectMode.UNBURNT_ONLY);
Set<PartInput> unburntTreeInputSet = new LinkedHashSet<PartInput>(unburntTreeInputList);
// Get the old tree inputs.
List<PartInput> oldUnburntTreeInputList = getOldUnburntTreeInputList(changedGem, oldInputs);
// Get the old targetable inputs.
List<PartInput> oldTargetableGemInputs = new ArrayList<PartInput>();
for (final PartInput oldInput : oldInputs) {
if (!oldInput.isConnected() && !oldInput.isBurnt()) {
oldTargetableGemInputs.add(oldInput);
}
}
// Get the updated targetable inputs.
Set<PartInput> updatedTargetableInputSet = new LinkedHashSet<PartInput>();
int nArgs = changedGem.getNInputs();
for (int i = 0; i < nArgs; i++) {
PartInput updatedGemInput = changedGem.getInputPart(i);
if (!updatedGemInput.isConnected() && !updatedGemInput.isBurnt()) {
updatedTargetableInputSet.add(updatedGemInput);
}
}
// Get the new (untargeted) arguments.
List<PartInput> untargetedNewArguments = new ArrayList<PartInput>(updatedTargetableInputSet);
untargetedNewArguments.removeAll(oldTargetableGemInputs);
// Find the best collector to target.
// This should only be null if there were no targetable inputs on the tree or the gem which changed.
CollectorGem collectorToTarget = oldTargetableGemInputs.isEmpty() ? getCollectorToTarget(oldUnburntTreeInputList)
: getCollectorToTarget(oldTargetableGemInputs);
if (collectorToTarget == null) {
collectorToTarget = GemGraph.obtainOutermostCollector(changedGem);
}
// Get the collector's old targeting arguments.
List<PartInput> targetArgs = collectorToTarget.getTargetArguments();
int nTargetArgs = targetArgs.size();
// Get the tree arguments which used to target the collector to target, in order.
List<PartInput> oldTargetingTreeInputs = new ArrayList<PartInput>();
for (final PartInput unburntTreeInput : oldUnburntTreeInputList) {
if (GemGraph.getInputArgumentTarget(unburntTreeInput) == collectorToTarget) {
oldTargetingTreeInputs.add(unburntTreeInput);
}
}
// Calculate characteristics of the previous tree inputs - whether they were together, and/or in natural order.
Pair<Boolean, Boolean> oldTreeInputsTogetherOrInNaturalOrder = isTogetherOrInNaturalOrder(oldTargetingTreeInputs, targetArgs);
boolean oldTreeInputsTogether = oldTreeInputsTogetherOrInNaturalOrder.fst().booleanValue();
boolean oldTreeInputsInNaturalOrder = oldTreeInputsTogetherOrInNaturalOrder.snd().booleanValue();
// Gem the gem inputs which carry over, in order.
List<PartInput> remainingOldGemInputList = new ArrayList<PartInput>(unburntTreeInputSet);
remainingOldGemInputList.retainAll(oldTargetableGemInputs);
// Determine if they occur together in natural order (or just together) in the targeted collector's target arguments.
Pair<Boolean, Boolean> isTogetherOrInNaturalOrder = isTogetherOrInNaturalOrder(oldTargetableGemInputs, targetArgs);
boolean isTogether = isTogetherOrInNaturalOrder.fst().booleanValue();
boolean isInNaturalOrder = isTogetherOrInNaturalOrder.snd().booleanValue();
// If the old inputs (on the gem or on the tree) were in natural order, ensure that they remain so.
if (oldTreeInputsInNaturalOrder) {
collectorToTarget.removeArguments(targetArgs);
targetArgs = getTargetArgsInOrder(targetArgs, unburntTreeInputList);
collectorToTarget.addArguments(targetArgs);
// The collector to target will also be affected..
affectedCollectorSet.add(collectorToTarget);
} else if (isInNaturalOrder) {
collectorToTarget.removeArguments(targetArgs);
targetArgs = getTargetArgsInOrder(targetArgs, Arrays.asList(changedGem.getInputParts()));
collectorToTarget.addArguments(targetArgs);
// The collector to target will also be affected..
affectedCollectorSet.add(collectorToTarget);
}
// Calculate the inputs which are going away.
Set<PartInput> departingInputs = new HashSet<PartInput>(oldTargetableGemInputs);
departingInputs.removeAll(updatedTargetableInputSet);
// If there aren't any untargeted new arguments, we're done.
if (untargetedNewArguments.isEmpty()) {
// Remove from their targets any arguments for inputs which are going away.
affectedCollectorSet.addAll(removeFromTargets(departingInputs));
return affectedCollectorSet;
}
//
// The rest of this method deals with where to place untargeted new arguments.
//
// The collector to target will also be affected..
affectedCollectorSet.add(collectorToTarget);
if (isTogether && isInNaturalOrder) {
// This is the argument on collectorToTarget before which this gem's args appear.
// Null means add to the beginning.
PartInput argBefore;
// Add the new args to wherever they appear in the tree input set.
PartInput firstUpdatedInput = updatedTargetableInputSet.iterator().next();
int firstUpdatedInputIndex = unburntTreeInputList.indexOf(firstUpdatedInput);
if (firstUpdatedInputIndex < 1) {
argBefore = null;
} else {
// Get the immediately preceding input on the tree which targets the same collector.
List<PartInput> precedingTreeInputs = unburntTreeInputList.subList(0, firstUpdatedInputIndex);
PartInput precedingTargetingInput = getTargetingArg(precedingTreeInputs, collectorToTarget, false);
if (precedingTargetingInput != null) {
// Add after the preceding tree input which targets the same collector.
argBefore = precedingTargetingInput;
} else {
// No preceding tree inputs target the same collector.
// Determine if any inputs which follow on the tree target the same collector.
PartInput lastUpdatedInput = (new ArrayList<PartInput>(updatedTargetableInputSet)).get(updatedTargetableInputSet.size() - 1);
int lastUpdatedInputIndex = unburntTreeInputList.indexOf(lastUpdatedInput);
List<PartInput> followingTreeInputs = unburntTreeInputList.subList(lastUpdatedInputIndex + 1, unburntTreeInputList.size());
PartInput followingTargetingInput = getTargetingArg(followingTreeInputs, collectorToTarget, false);
if (followingTargetingInput != null) {
// Add before the following tree input which targets the same collector.
int followingArgTargetIndex = targetArgs.indexOf(followingTargetingInput);
// Construct the reverse list of preceding targeting args.
List<PartInput> reversedPrecedingTargetArgs = new ArrayList<PartInput>(targetArgs.subList(0, followingArgTargetIndex));
Collections.reverse(reversedPrecedingTargetArgs);
// Set the arg before to the last preceding arg which isn't an input to the changed gem.
// If there isn't any such arg, add to the beginning.
argBefore = null;
for (final PartInput precedingTargetArg : reversedPrecedingTargetArgs) {
if (precedingTargetArg.getGem() != changedGem) {
argBefore = precedingTargetArg;
break;
}
}
} else {
// Add to the end of the collector's args.
argBefore = targetArgs.get(nTargetArgs - 1);
}
}
}
// remove all the old args..
collectorToTarget.removeArguments(remainingOldGemInputList);
// add all the updated args back..
collectorToTarget.addArguments(argBefore, updatedTargetableInputSet, argBefore != null);
} else if (isInNaturalOrder) {
// The old inputs appear in natural order in the target arg list.
// Maintain natural order among new inputs.
// Get the tree arguments which will target the collector to target.
List<PartInput> newTargetingTreeInputs = new ArrayList<PartInput>();
for (final PartInput unburntTreeInput : unburntTreeInputList) {
if (GemGraph.getInputArgumentTarget(unburntTreeInput) == collectorToTarget || unburntTreeInput.getGem() == changedGem) {
newTargetingTreeInputs.add(unburntTreeInput);
}
}
// If the old tree inputs appeared together in natural order, replace with the new tree inputs.
if (oldTreeInputsTogether && oldTreeInputsInNaturalOrder) {
// Remove the arguments, add them back at the appropriate location.
int addIndex = targetArgs.indexOf(oldTargetingTreeInputs.get(0));
collectorToTarget.removeArguments(oldTargetingTreeInputs);
collectorToTarget.addArguments(addIndex, newTargetingTreeInputs);
} else if (oldTreeInputsInNaturalOrder) {
// If the tree inputs just appear in natural order but not together, insert semi-smartly.
for (final PartInput newInput : untargetedNewArguments) {
// If no preceding tree args, add before the first of the args which targeted the collector.
if (newInput == newTargetingTreeInputs.get(0)) {
PartInput firstTargetingArg = oldTargetingTreeInputs.get(0);
collectorToTarget.addArguments(firstTargetingArg, Collections.singleton(newInput), false);
} else {
// Add after the arg which precedes it.
PartInput precedingTargetingTreeInput = newTargetingTreeInputs.get(newTargetingTreeInputs.indexOf(newInput) - 1);
collectorToTarget.addArguments(precedingTargetingTreeInput, Collections.singleton(newInput), true);
}
}
} else {
// Otherwise, the tree inputs are not in natural order, but the old gem inputs are.
// Add to the end.
// Note: it would be better to maintain order where this makes sense
// (eg. if intervening gem tree inputs appear together but are reordered wrt each other).
// Now add the arguments to the collector, at the end of the target arg list.
collectorToTarget.addArguments(untargetedNewArguments);
}
} else {
// The remaining args are not in natural order on target args.
// This is the argument on collectorToTarget before which the new args appear.
// Null means add to the end.
PartInput argBefore;
if (isTogether) {
// Add after the last of the target's arguments that belongs to an input on this gem.
argBefore = null;
for (final PartInput targetArg : targetArgs) {
if (targetArg.getGem() == changedGem) {
argBefore = targetArg;
}
}
} else {
// Add to the end.
argBefore = null;
}
// Now add the untargeted new arguments to the collector, at the appropriate location.
collectorToTarget.addArguments(argBefore, untargetedNewArguments, true);
}
// Remove from their targets any arguments for inputs which are going away.
affectedCollectorSet.addAll(removeFromTargets(departingInputs));
return affectedCollectorSet;
}
/**
* @param oldTargetArgs the old target arguments
* @param updatedTargetingInputs arguments, in the order in which they appear as updated.
* @return a list of arguments from targetArgs, reordered such that args also appearing in updatedTargetingInputs
* appear in their new order.
*/
private static List<PartInput> getTargetArgsInOrder(List<PartInput> oldTargetArgs, List<PartInput> updatedTargetingInputs) {
// make a copy of the target args array.
List<PartInput> newTargetArgs = new ArrayList<PartInput>(oldTargetArgs);
// Get the args which appear in both sets, in their old order.
List<PartInput> oldIntersection = new ArrayList<PartInput>(oldTargetArgs);
oldIntersection.retainAll(updatedTargetingInputs);
if (!oldIntersection.isEmpty()) {
// Get the index of the first of the targeting inputs in the old target args list.
int firstIndex = oldTargetArgs.indexOf(oldIntersection.get(0));
// Get the intersection, in the new order.
List<PartInput> newIntersection = new ArrayList<PartInput>(updatedTargetingInputs);
newIntersection.retainAll(oldTargetArgs);
if (!newIntersection.equals(oldIntersection)) {
// For now, just put all the intersecting args together, and put anything that might lie among those at the end.
newTargetArgs.removeAll(newIntersection);
newTargetArgs.addAll(firstIndex, newIntersection);
}
}
return newTargetArgs;
}
/**
* Return whether a given list's items appear together in another list, or appear in order in the other list.
* @param subList the list in question.
* @param superList the list which contains the first list.
* @return Pair:
* The first item is whether the items appear together, the second is whether they appear in order.
* Both false if subList is not a sublist of superList, or if the intersection of subList and superList is empty.
*/
private static Pair<Boolean, Boolean> isTogetherOrInNaturalOrder(List<PartInput> subList, List<PartInput> superList) {
// Copy the superList, and cut it down to only those items in the sublist.
List<PartInput> cutSuperList = new ArrayList<PartInput>(superList);
cutSuperList.retainAll(subList);
// Check for list precondition, non-intersecting lists.
if (cutSuperList.isEmpty() || cutSuperList.size() != subList.size()) {
return new Pair<Boolean, Boolean>(Boolean.FALSE, Boolean.FALSE);
}
// Figure out if the items are in natural order.
boolean isInNaturalOrder = cutSuperList.equals(new ArrayList<PartInput>(subList));
// Calculate the bounds of a sublist the length of subList, starting from the index of the first item of subList in superList.
int firstIndex = subList.isEmpty() ? -1 : superList.indexOf(subList.get(0));
int lastIndex = firstIndex + subList.size();
// Figure out if the items are together.
boolean isTogether = firstIndex >= 0 && lastIndex < superList.size() && superList.subList(firstIndex, lastIndex).containsAll(subList);
// Return the result.
return new Pair<Boolean, Boolean>(Boolean.valueOf(isTogether), Boolean.valueOf(isInNaturalOrder));
}
/**
* Get the first or last arg in the list which targets the given collector.
* @param argList the inputs to check.
* @param collectorTarget the target.
* @param first if true, returns the first arg which targets collectorTarget. If false, returns the last one.
* @return the first or last arg in the list which targets the given collector.
*/
private static PartInput getTargetingArg(List<PartInput> argList, CollectorGem collectorTarget, boolean first) {
// If we are getting the last arg, reverse the list.
if (!first) {
argList = new ArrayList<PartInput>(argList);
Collections.reverse(argList);
}
// Iterate over the list, returning the first arg which targets the collector.
for (final PartInput arg : argList) {
if (GemGraph.getInputArgumentTarget(arg) == collectorTarget) {
return arg;
}
}
// No args in the list target the collector.
return null;
}
/**
* Given a set of arguments which target some collector(s), determine the best collector for any new inputs to target.
* Assumption: all the inputs in the list to consider are in the same gem tree, and have not been disconnected from their targets.
*
* @param inputsToConsider the inputs to consider when calculating the best collector
* for their arguments to target. eg. if these all target a given collector, that collector will be returned.
* @return CollectorGem the best collector for an input to target, considering the given inputs.
* The target gem if the inputs target more than one collector.
* Null if the inputs have no enclosing collectors.
*/
private static CollectorGem getCollectorToTarget(Collection<PartInput> inputsToConsider) {
// If all the arguments target a collector, use that one. Otherwise use the outermost collector.
CollectorGem collectorToTarget = null;
boolean firstIteration = true;
for (final PartInput nextOldGemInput : inputsToConsider) {
CollectorGem inputTargetedCollector = GemGraph.getInputArgumentTarget(nextOldGemInput);
if (firstIteration) {
// the first iteration..
collectorToTarget = inputTargetedCollector;
firstIteration = false;
} else if (collectorToTarget != inputTargetedCollector) {
// Input args target more than one collector. Return the outermost collector (if any).
return GemGraph.obtainOutermostCollector(nextOldGemInput.getGem());
} else {
// this arg targets the same collector as all the other ones.
// Do nothing.
}
}
return collectorToTarget;
}
/**
* Retarget the arguments for a set of inputs according to the tree on which they appear.
* When connecting or disconnecting, a gem tree will gain some inputs, which remain to be retargeted.
* This method attempts to infer argument information (eg. arg position) from the inputs around the new inputs.
* For instance, if an input coming before the set of added inputs (with respect to the tree) targets the target
* collector, and the added inputs are determined to also target the target collector, the arguments for the added
* inputs will be placed after the argument for the input which precedes it in the tree.
*
* @param newInputList the list of inputs on the tree.
* @param firstNewInputIndex the index of the first new input whose argument should be retargeted.
* @param lastNewInputIndex the index of the last new input whose argument should be retargeted.
* @return the CollectorGem to which the arguments were targeted, or null if no argument (re)targeting took place.
*/
private CollectorGem retargetInputArgumentsForTree(List<PartInput> newInputList, int firstNewInputIndex, int lastNewInputIndex) {
// Try to infer the position of the new arguments from the other inputs.
// The new arguments.
Set<PartInput> newArgSet = new LinkedHashSet<PartInput>();
// Gather the new args.
for (final PartInput newInput : newInputList.subList(firstNewInputIndex, lastNewInputIndex + 1)) {
newArgSet.add(newInput);
}
if (newArgSet.isEmpty()) {
return null;
}
// Determine the new arguments
// TODOEL: If all the arguments in this subtree are targeting a collector, should that be the collectorToTarget?
// We should at least do this in the case where there is only one replacing input.
Gem rootGem = newArgSet.iterator().next().getGem().getRootGem();
CollectorGem collectorToTarget = (!(rootGem instanceof CollectorGem)) ? null : targetCollector; // Can this (sometimes) be rootGem?
if (collectorToTarget == null) {
return null;
}
// Now add the arguments to the target.
// Walk backwards over the inputs coming before the replaced input, looking for an input which targets the target collector.
// If we find one, add all the arguments after that input.
List<PartInput> beforeInputs = newInputList.subList(0, firstNewInputIndex);
PartInput beforeInputArray[] = (new ArrayList<PartInput>(beforeInputs)).toArray(new PartInput[firstNewInputIndex]);
for (int i = firstNewInputIndex - 1; i > -1; i--) {
PartInput inputArgument = beforeInputArray[i];
if (GemGraph.getInputArgumentTarget(inputArgument) == collectorToTarget) {
collectorToTarget.addArguments(inputArgument, newArgSet, true);
return collectorToTarget;
}
}
// Walk forward over the inputs coming after the replaced input, looking for an input which targets the target collector.
// If we find one, add all the arguments before that input.
if (lastNewInputIndex + 1 < newInputList.size()) {
List<PartInput> afterInputs = newInputList.subList(lastNewInputIndex + 1, newInputList.size());
for (final PartInput inputArgument : afterInputs) {
if (GemGraph.getInputArgumentTarget(inputArgument) == collectorToTarget) {
collectorToTarget.addArguments(inputArgument, newArgSet, false);
return collectorToTarget;
}
}
}
// Just add the args wherever the CALSourceGenerator would naturally put them.
addTargetArgsInSourceOrder(collectorToTarget, newArgSet);
return collectorToTarget;
}
/**
* Add the given arguments to the target, in the order in which they appear in the source.
* If they do not appear in the source, they are simply added to the end of the target's argument list.
* @param collectorToTarget the target to which the arguments will be added.
* @param newArgSet the arguments which are retargeted to the target collector.
* Must not be empty.
*/
private void addTargetArgsInSourceOrder(CollectorGem collectorToTarget, Set<PartInput> newArgSet) {
// Get the arguments in the order returned by the CALSourceGenerator.
CompositionNode.CompositionArgument[] argsFromSourceGen = CALSourceGenerator.getFunctionArguments(collectorToTarget);
CompositionNode.CompositionArgument firstArg = newArgSet.iterator().next();
// Find where the first argument appears.
int firstArgPosition = Arrays.asList(argsFromSourceGen).indexOf(firstArg);
if (firstArgPosition < 0) {
// The args don't appear in the source. Add to the end.
collectorToTarget.addArguments(newArgSet);
} else if (firstArgPosition == 0) {
// Add to the beginning
collectorToTarget.addArguments(0, newArgSet);
} else {
// Add after the argument which comes before it in source gen.
List<PartInput> targetedCollectorArgList = collectorToTarget.getTargetArguments();
PartInput argBefore = (PartInput)argsFromSourceGen[firstArgPosition - 1];
if (targetedCollectorArgList.contains(argBefore)) {
collectorToTarget.addArguments(argBefore, newArgSet, true);
} else {
// The "argBefore" doesn't appear in the list if it's an unaccounted-for arg.
collectorToTarget.addArguments(newArgSet);
}
}
}
/**
* Update the gem graph to reflect any changes in the inputs targeting the given collectors.
* This includes updating their reflected inputs, updating their reflectors, and propagating new targeting changes
* which might come about from newly-created reflector inputs.
* eg. If a reflector update would cause an argument to be added to a collector gem, that collector's reflectors
* will be updated as well (and so on..).
*
* @param affectedCollectors the collectors for which reflectors will be updated.
*/
void updateForArgumentChange(Set<CollectorGem> affectedCollectors) {
if (disableArgumentUpdating) {
return;
}
for (final CollectorGem affectedCollector : affectedCollectors) {
Map<ReflectorGem, PartInput[]> affectedReflectorsToOldInputsMap = affectedCollector.updateReflectedInputs();
// Take care of updating arguments for collectors affected by additional reflector inputs.
// Note that an infinite loop shouldn't occur, since new reflector inputs would be targeted at the target gem.
for (final Map.Entry<ReflectorGem, PartInput[]> mapEntry : affectedReflectorsToOldInputsMap.entrySet()) {
ReflectorGem reflectorGem = mapEntry.getKey();
retargetArgumentsForDefinitionChange(reflectorGem, mapEntry.getValue());
}
}
}
}