/*******************************************************************************
* ALMA - Atacama Large Millimeter Array
* Copyright (c) ESO - European Southern Observatory, 2011
* (in the framework of the ALMA collaboration).
* All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*******************************************************************************/
package alma.acs.nc.sm.generic;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.scxml.ErrorReporter;
import org.apache.commons.scxml.EventDispatcher;
import org.apache.commons.scxml.SCInstance;
import org.apache.commons.scxml.SCXMLExpressionException;
import org.apache.commons.scxml.TriggerEvent;
import org.apache.commons.scxml.model.CustomAction;
import org.apache.commons.scxml.model.ModelException;
import org.apache.commons.scxml.semantics.ErrorConstants;
import alma.ACSErrTypeCommon.wrappers.AcsJStateMachineActionEx;
/**
* The user-supplied dispatcher that will be invoked by an
* instance of {@link AcsScxmlDispatchingAction},
* thus bringing action handlers under user control
* instead of using directly the handler instantiated by the SCXML framework.
* <p>
* Handlers must be registered using {@link #registerActionHandler(Enum, AcsScxmlActionExecutor)}.
*
* @author hsommer
* @param <A> The SM action enumeration.
*/
public class AcsScxmlActionDispatcher<A extends Enum<A>>
{
public static final String CUSTOM_ACTIONS_DOMAIN_NAME = "http://my.custom-actions.domain/CUSTOM";
protected final Logger logger;
private final Class<A> actionType;
private final Map<A, AcsScxmlActionExecutor<A>> actionMap;
/**
* Optionally set by method {@link #setActionExceptionHandler(ActionExceptionHandler)}.
*/
private volatile ActionExceptionHandler actionExceptionHandler;
public AcsScxmlActionDispatcher(Logger logger, Class<A> actionType) {
this.logger = logger;
this.actionType = actionType;
actionMap = new EnumMap<A, AcsScxmlActionExecutor<A>>(actionType);
}
/**
* Generic action handler, dispatches to the right handler based on action type.
* <p>
* TODO: org.apache.commons.scxml.env.SimpleErrorReporter#onError prints the class name of the "Object errCtx".
* Currently we'd get NPE or useless info.
*/
public void execute(A action, final EventDispatcher evtDispatcher, final ErrorReporter errRep, final SCInstance scInstance,
final Collection<TriggerEvent> derivedEvents) throws ModelException, SCXMLExpressionException {
AcsScxmlActionExecutor<A> handler = getActionHandler(action);
if (handler != null) {
boolean handledAction = false;
try {
handledAction = handler.execute(action, evtDispatcher, errRep, scInstance, derivedEvents);
} catch (AcsJStateMachineActionEx ex) {
handledAction = true;
if (actionExceptionHandler != null) { // handler is optional
// ex will be thrown at the user
actionExceptionHandler.setActionException(ex);
}
else {
logger.log(Level.WARNING, "Handler " + handler.getClass() + " failed to execute action " + action.name(), ex);
}
// TODO: define and fire a standardized internal error event so that the state machine
// can reflect the problem and can be maneuvered out of the error again by the user
}
if (!handledAction) {
errRep.onError(ErrorConstants.UNKNOWN_ACTION, "Handler " + handler.getClass() + " handler unexpectedly did not handle action ", action);
}
}
else {
errRep.onError(ErrorConstants.UNKNOWN_ACTION, "No handler registered for action " + action.name(), null);
}
}
/**
* An alternative to {@link #execute(Enum, EventDispatcher, ErrorReporter, SCInstance, Collection)}
* where the handler can be accessed and stored directly.
* Using this approach may bring a tiny performance advantage, but probably should be removed again..
*/
public AcsScxmlActionExecutor<A> getActionHandler(A action) {
return actionMap.get(action);
}
/**
* Registers an action handler for the given SM action.
* Note that the same handler object can be registered for one or many actions,
* so that we are flexible in how we implement the handlers.
* @param action
* @param handler
*/
public void registerActionHandler(A action, AcsScxmlActionExecutor<A> handler) {
actionMap.put(action, handler);
}
/**
* Checks whether all action enums have an action handler registered.
* <p>
* This method can be used to validate early at runtime that no action goes
* without handler, which might happen after a change in the SM model.
* <p>
* Note that this kind of completeness check could also be done at build time
* using an action enum with visitor pattern, see
* http://raginggoblin.wordpress.com/2010/06/10/switch-over-all-values-of-a-java-enum/.
* However, this would require us to dispatch all actions through a single interface
* that has a typed method per action and whose implementation could then of course
* dispatch further to various objects.
* For the time being we stay with the looser coupling of registering various action handler
* objects and validating them for completeness early at runtime (not waiting until
* some rare action finally occurs).
*
* @return true if for all SM actions we have a registered handler.
*/
public boolean isActionMappingComplete() {
for (A action : actionType.getEnumConstants()) {
if (!actionMap.containsKey(action)) {
logger.warning("No handler has been registered for SM action " + action.name() + ".");
return false;
}
}
return true;
}
/**
* Used by an {@link AcsScxmlDispatchingAction} object to convert its action name to the corresponding enum value.
* (This type information cannot be passed directly because Apache SCXML creates the action with an empty constructor.)
*/
public Class<A> getActionType() {
return actionType;
}
/**
* Wraps the registered action handlers in a form suitable for the SCXML framework.
* The framework will instantiate DispatchingScxmlAction for each action,
* which will then call back to {@link #execute(Enum, EventDispatcher, ErrorReporter, SCInstance, Collection)}.
* @return
*/
protected List<CustomAction> getScxmlActionMap() {
List<CustomAction> ret = new ArrayList<CustomAction>();
for (A action : actionMap.keySet()) {
ret.add(new CustomAction(CUSTOM_ACTIONS_DOMAIN_NAME, action.name(), AcsScxmlDispatchingAction.class));
}
return ret;
}
public static interface ActionExceptionHandler {
/**
* Callback method, which only gets called if an action terminates with an exception.
* @param ex
*/
public void setActionException(AcsJStateMachineActionEx ex);
}
public void setActionExceptionHandler(ActionExceptionHandler handler) {
actionExceptionHandler = handler;
}
}