/* * 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. */ /* * Gem.java * Creation date: (12/11/00 8:21:31 AM) * By: Luke Evans */ 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 javax.swing.event.EventListenerList; import org.openquark.cal.compiler.CALSourceGenerator; import org.openquark.cal.compiler.CompilerMessageLogger; import org.openquark.cal.compiler.CompositionNode; import org.openquark.cal.compiler.MessageLogger; import org.openquark.cal.compiler.TypeException; import org.openquark.cal.compiler.TypeExpr; import org.openquark.cal.compiler.TypeChecker.TypeCheckInfo; import org.openquark.cal.metadata.ArgumentMetadata; import org.openquark.cal.services.CALFeatureName; import org.openquark.gems.client.GemGraph.GemVisitor; import org.openquark.util.Pair; import org.openquark.util.UnsafeCast; import org.openquark.util.xml.BadXMLDocumentException; import org.openquark.util.xml.NamespaceInfo; import org.openquark.util.xml.XMLPersistenceConstants; import org.openquark.util.xml.XMLPersistenceHelper; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * The Gem abstract base class. * Creation date: (12/11/00 8:21:31 AM) * @author Luke Evans */ public abstract class Gem implements CompositionNode { /** The output of the Gem */ private final PartOutput outputPart; /** The array of input parts */ private PartInput[] inputParts; /** Listener for state changes on this gem. */ GemStateListener gemStateListener; /** Listener for input burning changes on this gem. */ private BurnListener burnListener; /** Listener for changes in the name of the gem.*/ NameChangeListener nameChangeListener; /** A list of event listeners for this component. */ protected final EventListenerList listenerList = new EventListenerList(); /** An empty array of input parts. */ public static final PartInput[] EMPTY_INPUT_PART_ARRAY = new PartInput[0]; /** * Default constructor for a Gem. * @param nArgs the number of initial input parts. */ Gem(int nArgs) { // Create the output part. this.outputPart = createOutputPart(); // Create the input parts. PartInput[] newInputParts = new Gem.PartInput[nArgs]; for (int i = 0; i < nArgs; i++) { newInputParts[i] = createInputPart(i); } this.inputParts = newInputParts; } /** * Create an input part for this gem. * @param inputNum the index of the input to be created. * @return an input part to be used for this gem. */ protected PartInput createInputPart(int inputNum) { return new PartInput(inputNum); } /** * Create the output part for this gem. * This method will be called when the gem is created, to initialize the gem's output part. * @return the output part which will be used for this gem. */ protected PartOutput createOutputPart() { return new PartOutput(); } /** * Get the nth input associated with the gem. An exception is thrown if n is out of range. * @param n int the index of the input to return * @return PartInput the nth input in the gem. */ public final PartInput getInputPart(int n) { return inputParts[n]; } /** * Get the inputs for this gem. * @return PartInput[] the inputs for the gem */ public final PartInput[] getInputParts() { return (inputParts == null) ? EMPTY_INPUT_PART_ARRAY : (PartInput[])inputParts.clone(); } /** * Set the inputs for this gem. * @param newInputs the new inputs for the gem * @return whether the inputs changed. ie. whether the new inputs are different from the old ones. */ boolean setInputParts(PartInput[] newInputs){ PartInput[] oldInputs = inputParts; this.inputParts = newInputs; // notify listeners of any input change boolean inputsChanged = !Arrays.equals(oldInputs, this.inputParts); if (inputsChanged) { fireInputChangeEvent(oldInputs); } return inputsChanged; } /** * Returns the number of arguments accepted by this Gem * @return int the number of arguments */ public final int getNInputs() { return getInputParts().length; } /** * {@inheritDoc} */ public final int getNArguments() { return getNInputs(); } /** * {@inheritDoc} */ public CompositionNode.CompositionArgument getNodeArgument(final int i) { return getInputPart(i); } /** * Return the arguments as would be required by current definition of the Gem tree rooted at this gem. * @return the list of inputs required by the target. */ public final List<PartInput> getTargetInputs() { // Return the argument names in the order they would appear in the target sc. List<CompositionArgument> functionArgumentList = Arrays.asList(CALSourceGenerator.getFunctionArguments(this)); return new ArrayList<PartInput>(UnsafeCast.<List<PartInput>>unsafeCast(functionArgumentList)); // ~ unsafe } /** * Returns the output part. * @return PartOutput */ public final PartOutput getOutputPart() { return outputPart; } /** * Returns whether this Gem is at the root of the tree. * In dealing with collectors and emitters, this method returns the closest collector ancestor. * @return boolean true if this gem is at the root of the true */ public final boolean isRootGem() { return (getRootGem() == this); } /** * Returns the Gem at the root of the tree. * @return Gem the Gem at the root of the tree */ public Gem getRootGem() { // Get the output part Connection connectionToParent = outputPart.getConnection(); // If there is no connection from the output, the root Gem is this one if (connectionToParent == null) { return this; } // Follow the connection return connectionToParent.getDestination().getGem().getRootGem(); } /** * Returns the root collector for this gem. * The root collector is the collector at the root of the tree to which this gem is connected. * Note that unconnected emitters will return null, not their corresponding collectors. * @return CollectorGem the root collector for this gem, or null if the gem at the root is not a collector. */ public CollectorGem getRootCollectorGem() { if (this instanceof CollectorGem) { return (CollectorGem)this; } Gem outputGem = getOutputPart().getConnectedGem(); if (outputGem == null) { return null; } Gem outputRoot = outputGem.getRootGem(); if (outputRoot instanceof CollectorGem) { return (CollectorGem)outputRoot; } return null; } /** * Return whether this target is runnable * @return boolean whether this target is runnable */ public boolean isRunnable(){ // runnable if root gem, doesn't root a broken gem forest, and doesn't root a chain of only let's return isRootGem() && !GemGraph.isAncestorOfBrokenGemForest(this) && !GemGraph.isLetChainAncestor(this); } /** * Returns whether this gem is broken. * @return boolean true if this gem is broken */ public boolean isBroken() { // not broken by default return false; } /** * Returns whether this Gem is connected. * @return boolean true Gem is connected */ public final boolean isConnected(){ List<PartConnectable> connectableParts = getConnectableParts(); // check all the connectable parts to see if they are connected for (final PartConnectable part : connectableParts) { if (part.isConnected()) { return true; } } return false; } /** * Get the connectable parts on this gem. * * @return List the list of connectable parts on this gem. * The order is: input parts first (in order), then output part. */ public final List<PartConnectable> getConnectableParts() { List<PartConnectable> connectableParts = new ArrayList<PartConnectable>(); connectableParts.addAll(Arrays.asList(getInputParts())); if (outputPart != null) { connectableParts.add(outputPart); } return connectableParts; } /** * Get the type information from the parts of this Gem * @return TypeExpr[] the types. The order is: input first (in order), then output (if any). */ public final TypeExpr[] getPartTypes() { int numInputs = getNInputs(); boolean hasOutput = (outputPart != null); int numTypes = hasOutput ? numInputs + 1 : numInputs; TypeExpr[] tes = new TypeExpr[numTypes]; // Get the inputs for (int i = 0; i < numInputs; i++) { tes[i] = getInputPart(i).getType(); } // Add the output if any if (hasOutput) { tes[numInputs] = getOutputPart().getType(); } return tes; } /** * Set the output type of this root gem * @param outType TypeExpr the new output type of this gem (which is root) */ void setRootOutputType(TypeExpr outType){ if (getRootGem() != this) { throw new IllegalStateException("Can't set the root type on a non-root gem"); } getOutputPart().setType(outType); } /** * Get the result type of this gem. * @return TypeExpr the type of the result associated with this gem. */ public TypeExpr getResultType() { if (outputPart != null) { return outputPart.getType(); } return null; } /* * Methods implementing XMLPersistable ************************************************************ */ /** * Attach the saved form of this object as a child XML node. * @param parentNode the node that will be the parent of the generated XML. * The generated XML will be appended as a subtree of this node. * Note: parentNode must be a node type that can accept children (eg. an Element or a DocumentFragment) * @param gemContext the context in which the gem is saved. */ public void saveXML(Node parentNode, GemContext gemContext) { Document document = (parentNode instanceof Document) ? (Document)parentNode : parentNode.getOwnerDocument(); // Create the gem element Element resultElement = document.createElement(GemPersistenceConstants.GEM_TAG); parentNode.appendChild(resultElement); // Attach an attribute for the unique gem identifier. String gemIdentifier = gemContext.getIdentifier(this, true); resultElement.setAttribute(GemPersistenceConstants.GEM_ID_ATTR, gemIdentifier); // Add an element for inputs (if the gem can have inputs). if (inputParts != null) { Element inputsElement = document.createElement(GemPersistenceConstants.INPUTS_TAG); resultElement.appendChild(inputsElement); // Add child elements for each input int numChildren = getNInputs(); for (int i = 0; i < numChildren; i++) { PartInput input = getInputPart(i); input.saveXML(inputsElement); } } } /** * Load this object's state. * @param element Element the element representing the structure to deserialize. * @param gemContext the context in which the gem is being instantiated. */ void loadXML(Element element, GemContext gemContext) throws BadXMLDocumentException { XMLPersistenceHelper.checkTag(element, GemPersistenceConstants.GEM_TAG); // Add to the context, if an id attribute is present (otherwise, it's in the old save format..). String gemId = element.getAttribute(GemPersistenceConstants.GEM_ID_ATTR); if (gemId.equals("")) { throw new BadXMLDocumentException(element, "Missing gem id."); } gemContext.addGem(this, gemId); // Create inputs if any. Element childNode = XMLPersistenceHelper.getChildElement(element, GemPersistenceConstants.INPUTS_TAG); if (childNode != null) { List<Element> inputNodes = XMLPersistenceHelper.getChildElements(childNode); int numInputNodes = inputNodes.size(); PartInput[] newInputs = new PartInput[numInputNodes]; for (int i = 0; i < numInputNodes; i++) { Element inputNode = inputNodes.get(i); PartInput input = createInputPart(i); input.loadXML(inputNode); newInputs[i] = input; } inputParts = newInputs; } else { // TODOEL: TEMP: until clients update their save code. if (!(this instanceof ValueGem) && inputParts == null) { inputParts = EMPTY_INPUT_PART_ARRAY; } } } /** * Get the identifier for the first gem element (by preorder traversal) descending from a given element. * @param gemAncestorElement the ancestor element of the gem to id. * @return the gem's identifier, or null if a gem descendant cannot be found with an appropriate attribute. */ public static String getGemId(Element gemAncestorElement) { // Get the descendant nodes with the gem tag. NodeList nodeList = gemAncestorElement.getElementsByTagName(GemPersistenceConstants.GEM_TAG); // Iterate over them, looking for an element with the id attribute. int nNodes = nodeList.getLength(); for (int i = 0; i < nNodes; i++) { Node node = nodeList.item(i); if (node instanceof Element) { String gemId = ((Element)node).getAttribute(GemPersistenceConstants.GEM_ID_ATTR); if (!gemId.equals("")) { return gemId; } } } return null; } /** * Obtain the XML namespace info for gems. * @return gem XML namespace info.. */ public static NamespaceInfo getNamespaceInfo() { return new NamespaceInfo(GemPersistenceConstants.GEM_NS, GemPersistenceConstants.GEM_NS_PREFIX); } /** * A utility function that finds the next available name for gems that must have exclusive names * For examples, if the desired name is value, but value, value1 and value2 is invalid (specified in * the invalidNames collection), then this method would return value3, if it is valid. * @param oldName the original name * @param invalidNames the collection of invalid names. */ static String getNextValidName(String oldName, Collection<String> invalidNames) { int i; String newName = oldName; // We figure out what the 'baseName' is. // For example, if the name is "value2", then the 'baseName' is "value" for (i = oldName.length() - 1; i >= 0; i--) { char currentChar = oldName.charAt(i); if (!Character.isDigit(currentChar)) { break; } } String intPart = oldName.substring(i+1); // If the old name was value2, we don't want to insert value1, so we start counting from 3; int j = intPart.length() > 0 ? Integer.parseInt(intPart) + 1 : 1; String baseName = oldName.substring(0, i + 1); while (invalidNames.contains(newName)) { newName = baseName + j; j++; } return newName; } /* * Methods to handle gem visitation *************************************************************** */ /** * Use a GemVisitor to visit this gem and its ancestors. * The order of immediate ancestor visitation is not guaranteed if there are multiple immediate ancestors. * @param gemVisitor GemVisitor the visitor to operate on this gem and its ancestors. */ public final void visitAncestors(GemVisitor gemVisitor){ visitAncestors(gemVisitor, new HashSet<Gem>()); } /** * Helper method for visitAncestors. * @param gemVisitor GemVisitor the visitor to operate on this gem and its ancestors. * @param gemsVisited Set the set of gems already visited */ void visitAncestors(GemVisitor gemVisitor, Set<Gem> gemsVisited){ // visit if (seeVisitor(gemVisitor, gemsVisited)) { return; } // Get the output connection, if any Connection outputConnection = (outputPart == null) ? null : outputPart.getConnection(); // If there is a connection from the output, follow it if (outputConnection != null) { outputConnection.getDestination().getGem().visitAncestors(gemVisitor, gemsVisited); } } /** * Use a GemVisitor to visit this gem and its descendants. * Algorithm is pre-order, first-to-last. * ie. this gem is visited first, followed by its first descendant. On this descendant, repeat the algo, then * move on to the other descendants in order. * Emitters' descendants are their corresponding collectors. * Reflectors' descendants are both gems connected to their inputs (considered first), then their corresponding collectors. * @param gemVisitor GemVisitor the visitor to operate on this gem and its descendants. */ public final void visitDescendants(GemVisitor gemVisitor){ visitDescendants(gemVisitor, new HashSet<Gem>()); } /** * Helper method for visitDescendants. * @param gemVisitor GemVisitor the visitor to operate on this gem and its descendants. * @param gemsVisited Set the set of gems already visited */ void visitDescendants(GemVisitor gemVisitor, Set<Gem> gemsVisited){ // visit if (seeVisitor(gemVisitor, gemsVisited)) { return; } // Follow any connections to descendants int numArgs = getNInputs(); for (int i = 0; i < numArgs; i++) { Connection inputConnection = getInputPart(i).getConnection(); if (inputConnection != null) { // Follow the connection inputConnection.getSource().getGem().visitDescendants(gemVisitor, gemsVisited); } } } /** * Use a GemVisitor to visit every gem in this tree or forest. * @param gemVisitor GemVisitor the visitor to operate on the gems in this tree or forest. */ public final void visitGraph(GemVisitor gemVisitor){ if (gemVisitor.getTraversalScope() == GemGraph.TraversalScope.FOREST) { visitConnectedTrees(gemVisitor, new HashSet<Gem>()); } else { // if it's a tree visitor, just descend from the root of the tree getRootGem().visitDescendants(gemVisitor, new HashSet<Gem>()); } } /** * Helper method for visitGraph - visit every Gem in the forest. * The algorithm: * Starting from a root, visit descendants. This will result in a call on any descendant roots. * Then call upon ancestor roots. * Don't forget about unconnected emitters (which aren't roots). * The order in which ancestor roots are visited is not guaranteed. * @param gemVisitor GemVisitor the visitor to operate on the gems in this forest. * @param gemsVisited Set the set of gems already visited */ void visitConnectedTrees(GemVisitor gemVisitor, Set<Gem> gemsVisited){ if (gemVisitor.getTraversalScope() != GemGraph.TraversalScope.FOREST) { throw new IllegalArgumentException("Expecting a forest visitor."); } // make sure we start from a root gem if this is the initial call Gem rootGem; if (gemsVisited.isEmpty() && ((rootGem = getRootGem()) != this)) { rootGem.visitConnectedTrees(gemVisitor, gemsVisited); return; } // visit if (seeVisitor(gemVisitor, gemsVisited)) { return; } // Follow input connections. int numArgs = getNInputs(); for (int i = 0; i < numArgs; i++) { Connection inputConnection = getInputPart(i).getConnection(); if (inputConnection != null) { inputConnection.getSource().getGem().visitConnectedTrees(gemVisitor, gemsVisited); } } } /** * Get the visitor to visit this gem if appropriate. Tell us if we shouldn't pass the * visitor on to other gems. * @param gemVisitor GemVisitor the visitor to operate on the gems in this forest. * @param gemsVisited Set the set of gems already visited * @return boolean true if the visitor is done with visiting */ final boolean seeVisitor(GemVisitor gemVisitor, Set<Gem> gemsVisited) { // check if we already visited this gem if (gemsVisited.contains(this)) { return true; } gemsVisited.add(this); // visit return gemVisitor.visitGem(this); } /* * Methods to handle listeners **************************************************************** */ /** * Adds the specified burn listener to receive burn events from this gem. * If l is null, no exception is thrown and no action is performed. * * @param l the burn listener. */ public synchronized void addBurnListener(BurnListener l) { if (l == null) { return; } burnListener = GemEventMulticaster.add(burnListener, l); } /** * Removes the specified burn listener so that it no longer receives burn events from this gem. * This method performs no function, nor does it throw an exception, if the listener specified by * the argument was not previously added to this component. * If l is null, no exception is thrown and no action is performed. * * @param l the burn listener. */ public synchronized void removeBurnListener(BurnListener l) { if (l == null) { return; } burnListener = GemEventMulticaster.remove(burnListener, l); } /** * Adds the specified name change listener to receive name change events from this input. * If l is null, no exception is thrown and no action is performed. * * @param l the name change listener. */ public synchronized void addNameChangeListener(NameChangeListener l) { if (l == null) { return; } nameChangeListener = GemEventMulticaster.add(nameChangeListener, l); } /** * Removes the specified name change listener so that it no longer receives name change events from this gem. * This method performs no function, nor does it throw an exception, if the listener specified by * the argument was not previously added to this component. * If l is null, no exception is thrown and no action is performed. * * @param l the name changelistener. */ public synchronized void removeNameChangeListener(NameChangeListener l) { if (l == null) { return; } nameChangeListener = GemEventMulticaster.remove(nameChangeListener, l); } /** * Adds the specified state change listener to receive state change events from this gem . * If l is null, no exception is thrown and no action is performed. * * @param l the state change listener. */ public synchronized void addStateChangeListener(GemStateListener l) { if (l == null) { return; } gemStateListener = GemEventMulticaster.add(gemStateListener, l); } /** * Removes the specified state change listener so that it no longer receives state change events from this gem. * This method performs no function, nor does it throw an exception, if the listener specified by * the argument was not previously added to this component. * If l is null, no exception is thrown and no action is performed. * * @param l the state change listener. */ public synchronized void removeStateChangeListener(GemStateListener l) { if (l == null) { return; } gemStateListener = GemEventMulticaster.remove(gemStateListener, l); } /** * Adds the specified listener to receive input change events from this gem. * @param l the input change listener */ public void addInputChangeListener(InputChangeListener l) { listenerList.add(InputChangeListener.class, l); } /** * Removes the specified input change listener so that it no longer receives input change events from this gem. * @param l the input change listener */ public void removeInputChangeListener(InputChangeListener l) { listenerList.remove(InputChangeListener.class, l); } /** * Fires an input change event. * @param oldParts the old input parts. */ private void fireInputChangeEvent(PartInput[] oldParts) { Object[] listeners = listenerList.getListenerList(); InputChangeEvent e = null; for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == InputChangeListener.class) { if (e == null) { // Lazily create the event: e = new InputChangeEvent(this, oldParts); } ((InputChangeListener) listeners[i + 1]).inputsChanged(e); } } } /** * Adds the specified listener to receive type change events from this gem. * @param l the type change listener */ public void addTypeChangeListener(TypeChangeListener l) { listenerList.add(TypeChangeListener.class, l); } /** * Removes the specified type change listener so that it no longer receives type change events from this gem. * @param l the type change listener */ public void removeTypeChangeListener(TypeChangeListener l) { listenerList.remove(TypeChangeListener.class, l); } /** * Fires a type change event. * @param partChanged the part whose type changed */ private void fireTypeChangeEvent(PartConnectable partChanged) { Object[] listeners = listenerList.getListenerList(); TypeChangeEvent e = null; for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == TypeChangeListener.class) { if (e == null) { // Lazily create the event: e = new TypeChangeEvent(partChanged); } ((TypeChangeListener) listeners[i + 1]).typeChanged(e); } } } /** * Adds the specified listener to receive input name events from this gem. * @param l the input name listener */ public void addInputNameListener(InputNameListener l) { listenerList.add(InputNameListener.class, l); } /** * Removes the specified input name listener so that it no longer receives input name events from this gem. * @param l the input name listener */ public void removeInputNameListener(InputNameListener l) { listenerList.remove(InputNameListener.class, l); } /** * Fires an input name event. * @param inputChanged the input whose name changed */ void fireInputNameEvent(PartInput inputChanged) { Object[] listeners = listenerList.getListenerList(); InputNameEvent e = null; for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == InputNameListener.class) { if (e == null) { // Lazily create the event: e = new InputNameEvent(inputChanged); } ((InputNameListener) listeners[i + 1]).inputNameChanged(e); } } } /** * Infer the type of a part according to the constraints imposed on the part's gem by its connections. * @param gemPart the part in question. * @param info the info object to be used during type checking. * @return the inferred type of the part. */ private TypeExpr inferPartType(Gem.PartConnectable gemPart, TypeCheckInfo info) { // if not connected - no types if (!gemPart.isConnected()) { return TypeExpr.makeParametricType(); } Gem gem = gemPart.getGem(); Connection connection = gemPart.getConnection(); // Special case for an input part connected to the output of a value gem. // The code after this will not properly calculate value gem output types with constrained typevars (eg. Num a => a). if (!(gemPart instanceof Gem.PartInput) && connection.getSource().getGem() instanceof ValueGem) { return connection.getSource().getType(); } // The names of the collectors in the connected graph. Set<String> collectorNames = new HashSet<String>(); // Iterate over the roots of the forest of which the gem is part (including broken ones..). // From this, populate the collector names set, and get a collector (any collector will do.). CollectorGem aCollector = null; for (final Gem rootGem : GemGraph.obtainForestRoots(gem, true)) { if (rootGem instanceof CollectorGem) { CollectorGem collectorGem = (CollectorGem)rootGem; collectorNames.add(collectorGem.getUnqualifiedName()); if (aCollector == null) { aCollector = collectorGem; } } } Set<Connection> tempConnectionsToDisconnect = new HashSet<Connection>(); // If there are no collectors in the forest, paste in a collector, and connect it to the root. if (aCollector == null) { aCollector = new CollectorGem(); aCollector.setName("anonymousCollector"); Gem.PartInput aCollectorInput = aCollector.getCollectingPart(); Gem rootGem = gem.getRootGem(); Gem.PartOutput rootGemOutput = rootGem.getOutputPart(); Connection tempConnection = new Connection(rootGemOutput, aCollectorInput); rootGemOutput.bindConnection(tempConnection); aCollectorInput.bindConnection(tempConnection); tempConnectionsToDisconnect.add(tempConnection); } // Get the outermost enclosing collector, which will be considered the target. CollectorGem graphTarget = GemGraph.obtainOutermostCollector(aCollector); // Create the dummy collector, set it as the target for the gem graph's target. // It's the enclosing collector, so the name doesn't have to be unique.. CollectorGem tempTarget = new CollectorGem(); tempTarget.setName("tempTarget"); CollectorGem oldGraphTargetTarget = graphTarget.getTargetCollectorGem(); graphTarget.setTargetCollector(tempTarget); // Create a code gem, with the same number of arguments as the gem for which types will be inferred. int nInputs = gem.getNInputs(); CodeGem tempCodeGem = new CodeGem(nInputs); // Connect to the dummy collector. Connection tempCodeGemConnection = new Connection(tempCodeGem.getOutputPart(), tempTarget.getCollectingPart()); tempCodeGem.getOutputPart().bindConnection(tempCodeGemConnection); tempTarget.getCollectingPart().bindConnection(tempCodeGemConnection); // Add the code gem's arguments to the dummy collector. Set<PartInput> codeGemInputArguments = new LinkedHashSet<PartInput>(); for (int i = 0; i < nInputs; i++) { codeGemInputArguments.add(tempCodeGem.getInputPart(i)); } tempTarget.addArguments(0, codeGemInputArguments); tempTarget.updateReflectedInputs(); // Create reflector gems: one to substitute in place of the gem whose types to infer, one to actually get the inferred types. ReflectorGem reflectorToSubstitute = new ReflectorGem(tempTarget); ReflectorGem typeInferenceReflector = new ReflectorGem(tempTarget); tempTarget.addReflector(reflectorToSubstitute); tempTarget.addReflector(typeInferenceReflector); // Connections on the gem whose types to infer. Set<Connection> oldConnections = new HashSet<Connection>(); // Maps temporary arguments to the collectors to which they were added, so they can be removed after. Map<Gem.PartInput, CollectorGem> tempArgumentToTargetCollectorMap = new HashMap<PartInput, CollectorGem>(); try { // Replace the output connection.. Connection outputConnection = gem.getOutputPart().getConnection(); if (outputConnection != null) { Connection tempConn = new Connection(reflectorToSubstitute.getOutputPart(), outputConnection.getDestination()); reflectorToSubstitute.getOutputPart().bindConnection(tempConn); outputConnection.getDestination().bindConnection(tempConn); // Check that it's not a connection from a temporary collector we created earlier, // when dealing with the case of no collectors in the forest. if (!tempConnectionsToDisconnect.contains(outputConnection)) { oldConnections.add(outputConnection); tempConnectionsToDisconnect.add(tempConn); } } // Replace the input connections and arguments.. for (int i = 0; i < nInputs; i++) { Gem.PartInput input = gem.getInputPart(i); Connection oldInputConnection = input.getConnection(); if (oldInputConnection == null) { // Add the code gem's input argument as an argument.. CollectorGem argumentTarget = GemGraph.getInputArgumentTarget(input); if (argumentTarget != null) { Gem.PartInput surrogateArgument = reflectorToSubstitute.getInputPart(i); argumentTarget.addArguments(input, Collections.singleton(surrogateArgument), false); tempArgumentToTargetCollectorMap.put(surrogateArgument, argumentTarget); } } else { // Substitute the connection. Gem.PartInput reflectorGemInput = reflectorToSubstitute.getInputPart(i); Connection tempConn = new Connection(oldInputConnection.getSource(), reflectorGemInput); reflectorGemInput.bindConnection(tempConn); oldInputConnection.getSource().bindConnection(tempConn); tempConnectionsToDisconnect.add(tempConn); oldConnections.add(oldInputConnection); } } // Get all related gem graph roots. Set<Gem> typeCheckForestRoots = GemGraph.obtainForestRoots(tempTarget, false); typeCheckForestRoots.addAll(GemGraph.obtainForestRoots(reflectorToSubstitute, false)); typeCheckForestRoots.add(typeInferenceReflector); Set<CollectorGem> enclosingCollectors = new HashSet<CollectorGem>(); for (final Gem root : typeCheckForestRoots) { if (root instanceof CollectorGem && !enclosingCollectors.contains(root)) { enclosingCollectors.addAll(GemGraph.obtainEnclosingCollectors(root)); } } for (final CollectorGem enclosingCollector : enclosingCollectors) { typeCheckForestRoots.addAll(GemGraph.obtainForestRoots(enclosingCollector, false)); } // Type check the forest. final Pair<Map<CompositionNode.CompositionArgument, TypeExpr>, Map<CompositionNode, List<TypeExpr>>> resultPair = info.getTypeChecker().checkGraph(typeCheckForestRoots, info.getModuleName(), new MessageLogger()); Map<CompositionNode, List<TypeExpr>> rootNodeMap = resultPair.snd(); // Get the inferred type for the gem. These will be the types on the reflector gem used for this purpose. List<TypeExpr> gemTypeList = rootNodeMap.get(typeInferenceReflector); // The inferred type will be null if the connected tree is broken. if (gemTypeList == null) { return null; } if (gemPart instanceof Gem.PartOutput) { // The output type. return gemTypeList.get(gemTypeList.size() - 1); } else { // The input type. int inputNum = ((Gem.PartInput)gemPart).getInputNum(); return gemTypeList.get(inputNum); } } catch (TypeException te) { // checkGraph() failed (shouldn't happen..) te.printStackTrace(); throw new IllegalStateException("inferPartType: Couldn't infer type."); } finally { for (final Connection conn : tempConnectionsToDisconnect) { conn.getSource().bindConnection(null); conn.getDestination().bindConnection(null); } GemGraph.reconnectConnections(oldConnections); graphTarget.setTargetCollector(oldGraphTargetTarget); for (final Map.Entry<PartInput, CollectorGem> mapEntry : tempArgumentToTargetCollectorMap.entrySet()) { Gem.PartInput surrogateArgument = mapEntry.getKey(); CollectorGem argumentTarget = mapEntry.getValue(); argumentTarget.removeArgument(surrogateArgument); } } } /** * A PartConnectable has a type and possibly a connection. * Creation date: (12/15/00 11:35:15 AM) * @author Luke Evans */ public abstract class PartConnectable { /** The part's type. */ private TypeExpr type; /** The part's connection, if any. */ private Connection boundConnection; /** * Get the Gem to which this PartConnectable belongs. * @return Gem the gem */ public final Gem getGem() { return Gem.this; } /** * Get the type of this part * @return the type expression */ public TypeExpr getType() { return type; } /** * Set the type of this part * @param newType TypeExpr the type expression to set */ void setType(TypeExpr newType) { this.type = newType; fireTypeChangeEvent(this); } /** * Check if this part is connected. * @return boolean whether it is already connected */ public final boolean isConnected() { return (boundConnection != null); } /** * Get the connection at this part. * @return conn Connection the connection bound to this part */ public final Connection getConnection() { return boundConnection; } /** * Bind a connection to this part. * @param conn Connection the connection to connect */ public void bindConnection(Connection conn) { boundConnection = conn; } /** * Returns the gem to which this part is connected, if any. * If unconnected, null is returned. */ public abstract Gem getConnectedGem(); } /** * A PartInput is a connectable part which is a sink (destination). * PartInputs are either burnt or free. Free PartInputs are associated with an enclosing collector * (via the CollectorArgument), which defines the function on which the associated argument appears. * Creation date: (12/11/00 10:55:53 AM) * @author Luke Evans */ public class PartInput extends PartConnectable implements CompositionArgument { /** The index of this input on the gem on which it appears. */ private int inputNum; /** Whether the input is burnt. */ private boolean burntState; /** * The name of the input as assigned by its gem. * That means it is the name used either in CAL code or the display name from * the gem's metadata. The actual base input name of this input will be this * name if there is no display name set in the input's design metadata. * * Note: This property does not need to be serialized. It is initialized by a gem * whenever it creates its inputs to match the latest information available. */ private String originalInputName = ArgumentMetadata.DEFAULT_ARGUMENT_NAME; /** The name of the argument. */ private ArgumentName argumentName = new ArgumentName(ArgumentMetadata.DEFAULT_ARGUMENT_NAME); /** The metadata for this input. */ private ArgumentMetadata metadata; /** * Construct an InputPart for the given input number. * @param inputNum int the input number for this PartInput */ PartInput(int inputNum) { this.inputNum = inputNum; this.burntState = false; this.metadata = new ArgumentMetadata(CALFeatureName.getArgumentFeatureName(inputNum), GemCutter.getLocaleFromPreferences()); } /** * Determines whether the input is still a valid input to use. Code gems and reflectors can * all have their inputs dynamically changed leaving other objects holding onto * invalid input parts. If the input no longer exists as part of the gem, or if it has been * reordered. Technically reordering might be allowed, but the PartInput object holds onto its * input number which will no longer match the gems ordering so we consider this state to be * invalid as well. * @return This method will return true if this input part is still a valid input * on the gem and false if the input part is no longer part of the gem. */ public boolean isValid() { return (inputNum < getNInputs() && this == Gem.this.getInputPart(inputNum)); } /** * Return the input number * @return the input number */ public final int getInputNum() { return inputNum; } /** * Set the input number * This should only be used in special cases where the input parts are being * reordered with respect to each other. * @param inputNum the input number */ void setInputNum(int inputNum) { this.inputNum = inputNum; ArgumentMetadata newMetadata = new ArgumentMetadata(CALFeatureName.getArgumentFeatureName(inputNum), this.metadata.getLocale()); this.metadata.copyTo(newMetadata); this.metadata = newMetadata; } /** * @return a copy of this input's name object */ public ArgumentName getArgumentName() { return new ArgumentName(argumentName); } /** * Sets the name of this input as defined by the gem the input belongs to. * This should either be the name from CAL code or the display name from * the gem's metadata. * @param inputName the name to use as the input's base name. If this is * null the default input name will be used. */ void setOriginalInputName(String inputName) { this.originalInputName = inputName != null ? inputName : ArgumentMetadata.DEFAULT_ARGUMENT_NAME; } /** * @return the original name of this input as defined by the gem the input belongs to */ public String getOriginalInputName() { return originalInputName; } /** * @return ArgumentMetadata a copy of this input's metadata */ public ArgumentMetadata getDesignMetadata() { return (ArgumentMetadata) metadata.copy(); } /** * @param metadata the new metadata metadata for this input (a safe copy will be made) */ public void setDesignMetadata(ArgumentMetadata metadata) { if (metadata == null) { throw new IllegalArgumentException("Cannot set metadata to null"); } metadata.copyTo(this.metadata); } /** * Return whether or not this input is burnt. * @return boolean whether or not this input is burnt */ public final boolean isBurnt(){ return burntState; } /** * Burn this input * @param burnt true to burn, false to unburn. */ public void setBurnt(boolean burnt){ // Do a quick safety check if (!isValid()) { throw new IllegalArgumentException("Attempt to modify an invalid input"); } if (burntState != burnt) { burntState = burnt; if (burnListener != null) { burnListener.burntStateChanged(new BurnEvent(PartInput.this)); } } } /** * See if toggling the burn state of this input would break the gem tree. * @param info the info to use for typing the tree. * @return boolean Returns true if burning the input will result in a valid gem graph and false * if burning the input will result in a broken gem graph */ public boolean burnBreaksGem(TypeCheckInfo info) { // Do a quick safety check if (!isValid()) { throw new IllegalArgumentException("Attempt to checking burning for an invalid input"); } // first see if the gem's output is connected to an input Gem gem = getGem(); PartOutput output = gem.getOutputPart(); Connection conn = output.getConnection(); if (conn == null) { return false; } PartInput destInput = conn.getDestination(); // get the inferred output type TypeExpr inferredOutputType = output.inferType(info); // store the initial burn state and disconnect (temporarily) boolean wasBurnt = isBurnt(); output.bindConnection(null); destInput.bindConnection(null); TypeExpr outputType; try { // Change the burn state // Note that we don't call setBurnt() since we don't want to fire events for this test burntState = !wasBurnt; // Get the type of the gem given it's new burnt state CompilerMessageLogger logger = new MessageLogger (); Set<Gem> forestRoots = GemGraph.obtainForestRoots(gem, false); final Pair<Map<CompositionArgument, TypeExpr>, Map<CompositionNode, List<TypeExpr>>> resultPair = info.getTypeChecker().checkGraph(forestRoots, info.getModuleName(), logger); Map<CompositionNode, List<TypeExpr>> rootNodeMap = resultPair.snd(); List<TypeExpr> typePieces = rootNodeMap.get(gem); // Get the resulting output type outputType = typePieces.get(typePieces.size() - 1); } catch (TypeException te) { // Burning the input produced an invalid gem graph so we simply return false. Note that // the finally block will reset the burnt state and connections for us return false; } finally { // reset to previous state burntState = wasBurnt; output.bindConnection(conn); destInput.bindConnection(conn); } // It's possible that the burnt input created a valid gem graph (no type exception thrown), // but the resulting type won't unify correctly so the burn is still not allowed return !GemGraph.typesWillUnify(outputType, inferredOutputType, info); } /** * Infer the type of the variable based on the gem tree connected to its input * @param info the info to use for typing the tree. * @return TypeExpr the inferred type of this variable, or null if inference fails (connected to a broken subtree). */ public TypeExpr inferType(TypeCheckInfo info) { // return the inferred output type. This will be null if broken. return inferPartType(this, info); } /** * @see Gem.PartConnectable#getConnectedGem() */ @Override public Gem getConnectedGem() { return super.isConnected() ? getConnection().getSource().getGem() : null; } /** * {@inheritDoc} */ public CompositionNode getConnectedNode() { return getConnectedGem(); } /** * Set the argument name object. * @param newArgumentName the new argument name */ public void setArgumentName(ArgumentName newArgumentName) { argumentName = new ArgumentName(newArgumentName); fireInputNameEvent(this); } /** * {@inheritDoc} */ public String getNameString() { return argumentName.getCompositeName(); } /** * {@inheritDoc} */ @Override public String toString() { return "PartInput#" + getInputNum() + ". Name=" + getArgumentName().getCompositeName() + ". Burnt=" + isBurnt() + ". Gem=" + getGem(); } /** * Methods implementing XMLPersistable ************************************************************ */ /** * Attached the saved form of this object as a child XML node. * @param parentNode Node the node that will be the parent of the generated XML. The generated XML will * be appended as a subtree of this node. * Note: parentNode must be a node type that can accept children (eg. an Element or a DocumentFragment) */ public void saveXML(Node parentNode) { Document document = (parentNode instanceof Document) ? (Document)parentNode : parentNode.getOwnerDocument(); Element resultElement = document.createElement(GemPersistenceConstants.INPUT_TAG); parentNode.appendChild(resultElement); // Add an attribute for burn status resultElement.setAttribute(GemPersistenceConstants.INPUT_BURNT_ATTR, isBurnt() ? XMLPersistenceConstants.TRUE_STRING : XMLPersistenceConstants.FALSE_STRING); // Store the input name. argumentName.saveXML(resultElement); } /** * Load this object's state. * @param element Element the element representing the structure to deserialize. */ void loadXML(Element element) throws BadXMLDocumentException { XMLPersistenceHelper.checkTag(element, GemPersistenceConstants.INPUT_TAG); burntState = XMLPersistenceHelper.getBooleanAttribute(element, GemPersistenceConstants.INPUT_BURNT_ATTR); List<Element> elements = XMLPersistenceHelper.getChildElements(element); Element child = elements.get(0); if (child.getLocalName().equals(GemPersistenceConstants.INPUT_NAME_TAG)) { // Load the input name. ArgumentName argName = getArgumentName(); argName.loadXML(child); setArgumentName(argName); this.originalInputName = argName.getBaseName(); } } } /** * A PartOutput is a connectable part which is a source/output. * Creation date: (12/11/00 10:54:46 AM) * @author Luke Evans */ public class PartOutput extends PartConnectable { /** * Default constructor for a PartOutput. */ protected PartOutput() { } /** * Infer the output type based on the gem tree connected to its output. * @param info the info to use for typing the tree. * @return TypeExpr the inferred type of the output, or null if broken subtree */ public TypeExpr inferType(TypeCheckInfo info) { // return the inferred input type. This will be null if broken. return inferPartType(this, info); } /** * @see org.openquark.gems.client.Gem.PartConnectable#getConnectedGem() */ @Override public Gem getConnectedGem() { return super.isConnected() ? getConnection().getDestination().getGem() : null; } } }