/******************************************************************************* * Copyright (c) 2016 Ecole Polytechnique de Montreal, Ericsson * * All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html ******************************************************************************/ package org.eclipse.tracecompass.internal.tmf.analysis.xml.core.model; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.osgi.util.NLS; import org.eclipse.tracecompass.common.core.NonNullUtils; import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.Activator; import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.module.IXmlStateSystemContainer; import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.stateprovider.TmfXmlStrings; import org.eclipse.tracecompass.tmf.core.event.ITmfEvent; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; /** * This Class implements a state machine (FSM) tree in the XML-defined state * system. * * @author Jean-Christian Kouame */ public class TmfXmlFsm { private final Map<String, TmfXmlState> fStatesMap; private final List<TmfXmlScenario> fActiveScenariosList; private final List<TmfXmlBasicTransition> fPreconditions; private final String fId; private final ITmfXmlModelFactory fModelFactory; private final IXmlStateSystemContainer fContainer; private final String fFinalStateId; private final String fAbandonStateId; private final boolean fInstanceMultipleEnabled; private final String fInitialStateId; private final boolean fConsuming; private boolean fEventConsumed; private int fTotalScenarios; private @Nullable TmfXmlScenario fPendingScenario; /** * Factory to create a {@link TmfXmlFsm} * * @param modelFactory * The factory used to create XML model elements * @param node * The XML root of this fsm * @param container * The state system container this fsm belongs to * @return The new {@link TmfXmlFsm} */ public static TmfXmlFsm create(ITmfXmlModelFactory modelFactory, Element node, IXmlStateSystemContainer container) { String id = node.getAttribute(TmfXmlStrings.ID); boolean consuming = node.getAttribute(TmfXmlStrings.CONSUMING).isEmpty() ? true : Boolean.parseBoolean(node.getAttribute(TmfXmlStrings.CONSUMING)); boolean instanceMultipleEnabled = node.getAttribute(TmfXmlStrings.MULTIPLE).isEmpty() ? true : Boolean.parseBoolean(node.getAttribute(TmfXmlStrings.MULTIPLE)); final List<@NonNull TmfXmlBasicTransition> preconditions = new ArrayList<>(); // Get the preconditions NodeList nodesPreconditions = node.getElementsByTagName(TmfXmlStrings.PRECONDITION); for (int i = 0; i < nodesPreconditions.getLength(); i++) { preconditions.add(new TmfXmlBasicTransition(((Element) NonNullUtils.checkNotNull(nodesPreconditions.item(i))))); } // Get the initial state and the preconditions Map<@NonNull String, @NonNull TmfXmlState> statesMap = new HashMap<>(); String initialState = node.getAttribute(TmfXmlStrings.INITIAL); NodeList nodesInitialElement = node.getElementsByTagName(TmfXmlStrings.INITIAL); NodeList nodesInitialStateElement = node.getElementsByTagName(TmfXmlStrings.INITIAL_STATE); if (nodesInitialStateElement.getLength() > 0) { if (!initialState.isEmpty() || nodesInitialElement.getLength() > 0) { Activator.logWarning("Fsm " + id + ": the 'initial' attribute was set or an <initial> element was defined. Only one of the 3 should be used."); //$NON-NLS-1$ //$NON-NLS-2$ } @NonNull TmfXmlState initial = modelFactory.createState((Element) nodesInitialStateElement.item(0), container, null); statesMap.put(TmfXmlState.INITIAL_STATE_ID, initial); initialState = TmfXmlState.INITIAL_STATE_ID; } else { if (!initialState.isEmpty() && nodesInitialElement.getLength() > 0) { Activator.logWarning("Fsm " + id + " was declared with both 'initial' attribute and <initial> element. Only the 'initial' attribute will be used"); //$NON-NLS-1$ //$NON-NLS-2$ } if (initialState.isEmpty() && nodesInitialElement.getLength() > 0) { NodeList nodesTransition = ((Element) nodesInitialElement.item(0)).getElementsByTagName(TmfXmlStrings.TRANSITION); if (nodesInitialElement.getLength() != 1) { throw new IllegalArgumentException("initial element : there should be one and only one initial state."); //$NON-NLS-1$ } initialState = ((Element) nodesTransition.item(0)).getAttribute(TmfXmlStrings.TARGET); } } // Get the FSM states NodeList nodesState = node.getElementsByTagName(TmfXmlStrings.STATE); for (int i = 0; i < nodesState.getLength(); i++) { Element element = (Element) NonNullUtils.checkNotNull(nodesState.item(i)); TmfXmlState state = modelFactory.createState(element, container, null); statesMap.put(state.getId(), state); // If the initial state was not already set, we use the first state // declared in the fsm description as initial state if (initialState.isEmpty()) { initialState = state.getId(); } } if (initialState.isEmpty()) { throw new IllegalStateException("No initial state has been declared in fsm " + id); //$NON-NLS-1$ } // Get the FSM final state String finalStateId = TmfXmlStrings.NULL; NodeList nodesFinalState = node.getElementsByTagName(TmfXmlStrings.FINAL); if (nodesFinalState.getLength() == 1) { final Element finalElement = NonNullUtils.checkNotNull((Element) nodesFinalState.item(0)); finalStateId = finalElement.getAttribute(TmfXmlStrings.ID); if (!finalStateId.isEmpty()) { TmfXmlState finalState = modelFactory.createState(finalElement, container, null); statesMap.put(finalState.getId(), finalState); } } // Get the FSM abandon state String abandonStateId = TmfXmlStrings.NULL; NodeList nodesAbandonState = node.getElementsByTagName(TmfXmlStrings.ABANDON_STATE); if (nodesAbandonState.getLength() == 1) { final Element abandonElement = NonNullUtils.checkNotNull((Element) nodesAbandonState.item(0)); abandonStateId = abandonElement.getAttribute(TmfXmlStrings.ID); if (!abandonStateId.isEmpty()) { TmfXmlState abandonState = modelFactory.createState(abandonElement, container, null); statesMap.put(abandonState.getId(), abandonState); } } return new TmfXmlFsm(modelFactory, container, id, consuming, instanceMultipleEnabled, initialState, finalStateId, abandonStateId, preconditions, statesMap); } private TmfXmlFsm(ITmfXmlModelFactory modelFactory, IXmlStateSystemContainer container, String id, boolean consuming, boolean multiple, String initialState, String finalState, String abandonState, List<TmfXmlBasicTransition> preconditions, Map<String, TmfXmlState> states) { fModelFactory = modelFactory; fTotalScenarios = 0; fContainer = container; fId = id; fConsuming = consuming; fInstanceMultipleEnabled = multiple; fInitialStateId = initialState; fFinalStateId = finalState; fAbandonStateId = abandonState; fPreconditions = ImmutableList.copyOf(preconditions); fStatesMap = ImmutableMap.copyOf(states); fActiveScenariosList = new ArrayList<>(); } /** * Get the fsm ID * * @return the id of this fsm */ public String getId() { return fId; } /** * Get the initial state ID of this fsm * * @return the id of the initial state of this finite state machine */ public String getInitialStateId() { return fInitialStateId; } /** * Get the final state ID of this fsm * * @return the id of the final state of this finite state machine */ public String getFinalStateId() { return fFinalStateId; } /** * Get the abandon state ID fo this fsm * * @return the id of the abandon state of this finite state machine */ public String getAbandonStateId() { return fAbandonStateId; } /** * Get the states table of this fsm in map * * @return The map containing all state definition for this fsm */ public Map<String, TmfXmlState> getStatesMap() { return Collections.unmodifiableMap(fStatesMap); } /** * Set whether the ongoing was consumed by a scenario or not * * @param eventConsumed * The consumed state */ public void setEventConsumed(boolean eventConsumed) { fEventConsumed = eventConsumed; } private boolean isEventConsumed() { return fEventConsumed; } /** * Process the active event and determine the next step of this fsm * * @param event * The event to process * @param tests * The list of possible transitions of the state machine * @param scenarioInfo * The active scenario details. * @return A pair containing the next state of the state machine and the * actions to execute */ public @Nullable TmfXmlStateTransition next(ITmfEvent event, Map<String, TmfXmlTransitionValidator> tests, TmfXmlScenarioInfo scenarioInfo) { boolean matched = false; TmfXmlStateTransition stateTransition = null; TmfXmlState state = fStatesMap.get(scenarioInfo.getActiveState()); if (state == null) { /** FIXME: This logging should be replaced by something the user will see, this is XML debugging information! */ Activator.logError(NLS.bind(Messages.TmfXmlFsm_StateUndefined, scenarioInfo.getActiveState(), getId())); return null; } for (int i = 0; i < state.getTransitionList().size() && !matched; i++) { stateTransition = state.getTransitionList().get(i); matched = stateTransition.test(event, scenarioInfo, tests); } return matched ? stateTransition : null; } /** * Validate the preconditions of this fsm. If not validate, the fsm will * skip the active event. * * @param event * The current event * @param tests * The transition inputs * @return True if one of the precondition is validated, false otherwise */ private boolean validatePreconditions(ITmfEvent event, Map<String, TmfXmlTransitionValidator> tests) { if (fPreconditions.isEmpty()) { return true; } for (TmfXmlBasicTransition precondition : fPreconditions) { if (precondition.test(event, null, tests)) { return true; } } return false; } /** * Handle the current event * * @param event * The current event * @param testMap * The transitions of the pattern */ public void handleEvent(ITmfEvent event, Map<String, TmfXmlTransitionValidator> testMap) { setEventConsumed(false); boolean isValidInput = handleActiveScenarios(event, testMap); handlePendingScenario(event, isValidInput); } /** * Process the active scenario with the ongoing event * * @param event * The ongoing event * @param testMap * The map of transition * @return True if the ongoing event validates the preconditions, false otherwise */ private boolean handleActiveScenarios(ITmfEvent event, Map<String, TmfXmlTransitionValidator> testMap) { if (!validatePreconditions(event, testMap)) { return false; } // The event is valid, we can handle the active scenario for (Iterator<TmfXmlScenario> currentItr = fActiveScenariosList.iterator(); currentItr.hasNext();) { TmfXmlScenario scenario = currentItr.next(); // Remove inactive scenarios or handle the active ones. if (!scenario.isActive()) { currentItr.remove(); } else { handleScenario(scenario, event); if (fConsuming && isEventConsumed()) { return true; } } } // The event is valid but hasn't been consumed. We return true. return true; } /** * Handle the pending scenario. * * @param event * The ongoing event * @param isInputValid * Either the ongoing event validated the preconditions or not */ private void handlePendingScenario(ITmfEvent event, boolean isInputValid) { if (fConsuming && isEventConsumed()) { return; } TmfXmlScenario scenario = fPendingScenario; if ((fInitialStateId.equals(TmfXmlState.INITIAL_STATE_ID) || isInputValid) && scenario != null) { handleScenario(scenario, event); if (!scenario.isPending()) { addActiveScenario(scenario); fPendingScenario = null; } } } /** * Abandon all ongoing scenarios */ public void dispose() { for (TmfXmlScenario scenario : fActiveScenariosList) { if (scenario.isActive()) { scenario.cancel(); } } } private static void handleScenario(TmfXmlScenario scenario, ITmfEvent event) { if (scenario.isActive() || scenario.isPending()) { scenario.handleEvent(event); } } /** * Create a new scenario of this fsm * * @param event * The current event, null if not * @param eventHandler * The event handler this fsm belongs * @param force * True to force the creation of the scenario, false otherwise */ public synchronized void createScenario(@Nullable ITmfEvent event, TmfXmlPatternEventHandler eventHandler, boolean force) { if (force || isNewScenarioAllowed()) { fPendingScenario = new TmfXmlScenario(event, eventHandler, fId, fContainer, fModelFactory); fTotalScenarios++; } } /** * Add a scenario to the active scenario list * * @param scenario * The scenario */ private void addActiveScenario(TmfXmlScenario scenario) { fActiveScenariosList.add(scenario); } /** * Check if we have the right to create a new scenario. A new scenario could * be created if it is not the first scenario of an FSM and the FSM is not a * singleton and the status of the last created scenario is not PENDING. * * @return True if the start of a new scenario is allowed, false otherwise */ public synchronized boolean isNewScenarioAllowed() { return fTotalScenarios > 0 && fInstanceMultipleEnabled && fPendingScenario == null; } }