/* * Copyright (C) 2012 Jason Gedge <http://www.gedge.ca> * * This file is part of the OpGraph project. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.gedge.opgraph; import java.util.Iterator; import java.util.NoSuchElementException; import ca.gedge.opgraph.exceptions.InvalidTypeException; import ca.gedge.opgraph.exceptions.ProcessingException; import ca.gedge.opgraph.exceptions.RequiredInputException; import ca.gedge.opgraph.extensions.CompositeNode; import ca.gedge.opgraph.extensions.CustomProcessing; import ca.gedge.opgraph.extensions.CustomProcessing.CustomProcessor; import ca.gedge.opgraph.validators.TypeValidator; /** * A processing context for {@link OpGraph} instances. A fine level of control * is given, allowing one to step through an operable graph in various ways. */ public class Processor { /** The graph this processor is operating on */ private OpGraph graph; /** Custom processing needs of the given graph */ private CustomProcessor customProcessor; /** * An iterator that points to the node we are currently processing on, * or <code>null</code> if processing hasn't begun/should be restarted. */ private Iterator<OpNode> nodeIter; /** The node we are operating on*/ private OpNode currentNode; /** The context map used for processing */ private OpContext globalContext; /** The error that happened in the last step, or <code>null</code> if no error */ private ProcessingException currentError; /** If we stepped into a macro, the processing context for that macro */ private Processor currentMacro; /** * Constructs a processing context for a given graph. * * @param graph the graph * * @throws NullPointerException if the specified graph is <code>null</code> */ public Processor(OpGraph graph) { this(graph, null, null); } /** * Constructs a processing context for a given graph and a preset operating context. * * @param graph the graph * @param context the initial global context, or <code>null</code> to * use an empty global context * * @throws NullPointerException if the specified graph is <code>null</code> */ public Processor(OpGraph graph, OpContext context) { this(graph, null, context); } /** * Constructs a processing context for a given graph. * * @param graph the graph * @param customProcessor a custom processing instance, or <code>null</code> * if no custom processing required * @param context the initial global context, or <code>null</code> to * use an empty global context * * @throws NullPointerException if the specified graph is <code>null</code> */ public Processor(OpGraph graph, CustomProcessor customProcessor, OpContext context) { if(graph == null) throw new NullPointerException("Graph cannot be null"); this.graph = graph; this.customProcessor = customProcessor; reset(context); } /** * Resets this context so that further processing will start from the * beginning. */ public void reset() { reset(globalContext == null ? null : globalContext); } /** * Resets this context so that further processing will start from the * beginning. * * @param context the global context that should be used for processing, * or <code>null</code> if a default one should be used */ public void reset(OpContext context) { currentMacro = null; currentError = null; currentNode = null; // Set up node iteration nodeIter = null; if(customProcessor != null) nodeIter = customProcessor; if(nodeIter == null) nodeIter = graph.getVertices().iterator(); // Set up context if(globalContext != null && globalContext == context) globalContext.clearChildContexts(); globalContext = context; if(globalContext == null) globalContext = new OpContext(); if(customProcessor != null) customProcessor.initialize(globalContext); } /** * Gets the graph that is currently being operated on. * * @return the graph */ public OpGraph getGraph() { if(currentMacro != null) return currentMacro.getGraph(); return graph; } /** * Gets the graph this processing context is operating on. * * @return the graph */ public OpGraph getGraphOfContext() { return graph; } /** * Gets the context used for processing. * * @return the context */ public OpContext getContext() { return globalContext; } /** * Gets the error that was thrown since the last reset. * * @return the error, or <code>null</code> if no error was thrown */ public ProcessingException getError() { ProcessingException error = currentError; if(error == null && currentMacro != null) error = currentMacro.getError(); return error; } /** * Gets the context that spawned the error returned by {@link #getError()}. * * @return the error-spawning context, or <code>null</code> if * <code>{@link #getError()} == null</code> */ public Processor getErrorContext() { if(currentError != null) return this; return (currentMacro == null ? null : currentMacro.getErrorContext()); } /** * Gets the node that was most recently processed. * * @return the node, or <code>null</code> if processing has yet to * start or no more nodes to process. */ public OpNode getCurrentNode() { if(currentMacro != null) return currentMacro.getCurrentNode(); return currentNode; } /** * Gets the node that was most recently processed in this context. * * @return the node, or <code>null</code> if processing has yet to * start or no more nodes to process. */ public OpNode getCurrentNodeOfContext() { return currentNode; } /** * Gets the processing context for the last macro that was stepped into. * * @return the processing context, or <code>null</code> if no macro was * stepped into */ public Processor getMacroContext() { return currentMacro; } /** * Gets whether or not there are any more nodes to process. * * @return <code>true</code> if there are more nodes to process, * <code>false</code> otherwise */ public boolean hasNext() { return (currentMacro != null || (nodeIter != null && nodeIter.hasNext())); } /** * Moves the processing forward. If in a macro and the last node in that * macro was already processed, the step will step out of the macro and * back to its parent node, but processing will not move forward in the * parent until this function is called again. * * @throws NoSuchElementException if there are no more nodes to process */ public void step() { if(currentMacro != null) { if(currentMacro.hasNext()) currentMacro.step(); else stepOutOf(); } else if(nodeIter == null) { throw new NoSuchElementException("No nodes to process"); } else { // Step to the next node and process currentNode = nodeIter.next(); processCurrentNode(); } } /** * Processes the current node. * * @throws ProcessingException if any errors occurred during proessing */ private void processCurrentNode() { try { final OpContext localContext = globalContext.getChildContext(currentNode); setupInputs(currentNode, localContext); Boolean enabled = (Boolean)localContext.get(OpNode.ENABLED_FIELD); if(enabled == null || enabled == Boolean.TRUE) currentNode.operate(localContext); if(!hasNext() && customProcessor != null) customProcessor.terminate(globalContext); } catch(ProcessingException exc) { //LOGGER.log(Level.SEVERE, exc.getLocalizedMessage(), exc); currentError = exc; nodeIter = null; // prevent further processing } catch(Throwable exc) { //LOGGER.log(Level.SEVERE, exc.getLocalizedMessage(), exc); currentError = new ProcessingException(exc); nodeIter = null; // prevent further processing } } /** * Processes the graph until we reach the next node level. */ public void stepToNextLevel() { if(currentMacro != null) { if(currentMacro.hasNext()) currentMacro.stepToNextLevel(); else stepOutOf(); } else { final int level = graph.getLevel(currentNode); while(hasNext() && graph.getLevel(currentNode) == level) step(); } } /** * Processes the graph until we hit the specified node. This method * will step through the current macro (if one was stepped into), but * will not automatically step into macros to find the specified node. * * @param node the node to stop processing at * * @return <code>true</code> if the specified node was found, * <code>false</code> otherwise */ public boolean stepToNode(OpNode node) { boolean found = false; if(currentMacro != null) { if(currentMacro.hasNext()) found = currentMacro.stepToNode(node); if(!found && !currentMacro.hasNext()) stepOutOf(); } if(!found) { while(hasNext() && currentNode != node) step(); found = (currentNode == node); } return found; } /** * Step into a macro. If the current node isn't a macro, this * function behaves exactly like {@link #step()}. * * @throws NoSuchElementException if no more nodes to process */ public void stepInto() { if(currentMacro != null) { if(currentMacro.hasNext()) currentMacro.stepInto(); else stepOutOf(); } else { currentNode = nodeIter.next(); final CompositeNode composite = currentNode.getExtension(CompositeNode.class); if(composite != null) { try { final OpContext context = globalContext.getChildContext(currentNode); setupInputs(currentNode, context); final CustomProcessing customProcessing = currentNode.getExtension(CustomProcessing.class); final CustomProcessor customProcessor = (customProcessing == null ? null : customProcessing.getCustomProcessor()); currentMacro = new Processor(composite.getGraph(), customProcessor, context); } catch(ProcessingException error) { currentError = error; currentMacro = null; // we didn't properly step into the macro, so null it nodeIter = null; // prevent further processing } } else { processCurrentNode(); } } } /** * Steps out of the current macro, if one was previously stepped into. Any * unprocessed nodes in the macro will be processed. If there was no macro * being processed by this processing context, this function does nothing. */ public void stepOutOf() { if(currentMacro != null) { if(currentMacro.getMacroContext() == null) { currentMacro.stepAll(); currentError = currentMacro.getError(); currentMacro = null; } else { currentMacro.stepOutOf(); } } } /** * Processes the graph to completion. */ public void stepAll() { while(hasNext()) step(); } /** * Adds inputs from incoming links to a given node's context. * * @param node the node to create the inputs for * @param context the working context for this node * * @throws ProcessingException if the node has no working context * @throws RequiredInputException if a value flowing into an input has an unacceptable type */ private void setupInputs(OpNode node, OpContext context) throws ProcessingException, RequiredInputException { // Check required inputs checkInputs(node, context); // Now set up the inputs for(OpLink link : graph.getIncomingEdges(node)) { final OpContext srcContext = globalContext.findChildContext(link.getSource()); if(srcContext != null && srcContext.containsKey(link.getSourceField())) { final Object val = srcContext.get(link.getSourceField()); final InputField dest = link.getDestinationField(); context.put(dest, val); } } } /** * Check field optionalities and make sure all required fields have input. * When input exists, make sure the input is of a valid type, as determined * by the field's {@link TypeValidator} * * @param node the node to create the inputs for * @param context the working context for this node * * @throws ProcessingException if the node has no working context * @throws RequiredInputException if a value flowing into an input has an unacceptable type */ private void checkInputs(OpNode node, OpContext context) throws InvalidTypeException, RequiredInputException { // for(InputField field : node.getInputFields()) { // Working context already has value, no need to check links if(context.containsKey(field)) continue; if(!field.isOptional()) { boolean linkFound = false; for(OpLink link : graph.getIncomingEdges(node)) { if(link.getDestinationField() == field) { // Make sure this link actually has a value flowing through it final OpContext sourceContext = globalContext.findChildContext(link.getSource()); if(sourceContext != null && sourceContext.containsKey(link.getSourceField())) { final Object val = sourceContext.get(link.getSourceField()); linkFound = true; // Make sure value type is accepted at the destination field final TypeValidator validator = field.getValidator(); if(validator != null && !validator.isAcceptable(val)) throw new InvalidTypeException(link.getDestinationField(), val); break; } } } // No link for required input; throw exception! if(!linkFound) throw new RequiredInputException(node, field); } } } }