/* * Copyright 2014 DataGenerator Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.finra.datagenerator.engine.scxml; import org.apache.commons.scxml.Context; import org.apache.commons.scxml.SCXMLExecutor; import org.apache.commons.scxml.SCXMLExpressionException; import org.apache.commons.scxml.env.jsp.ELContext; import org.apache.commons.scxml.env.jsp.ELEvaluator; import org.apache.commons.scxml.io.SCXMLParser; import org.apache.commons.scxml.model.Action; import org.apache.commons.scxml.model.Assign; import org.apache.commons.scxml.model.CustomAction; import org.apache.commons.scxml.model.ModelException; import org.apache.commons.scxml.model.OnEntry; import org.apache.commons.scxml.model.SCXML; import org.apache.commons.scxml.model.Transition; import org.apache.commons.scxml.model.TransitionTarget; import org.finra.datagenerator.distributor.SearchDistributor; import org.finra.datagenerator.engine.Engine; import org.finra.datagenerator.engine.Frontier; import org.finra.datagenerator.engine.scxml.tags.CustomTagExtension; import org.finra.datagenerator.engine.scxml.tags.FileExtension; import org.finra.datagenerator.engine.scxml.tags.RangeExtension; import org.finra.datagenerator.engine.scxml.tags.SetAssignExtension; import org.finra.datagenerator.engine.scxml.tags.SingleValueAssignExtension; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; /** * Engine implementation for generating data with SCXML state machine models. */ public class SCXMLEngine extends SCXMLExecutor implements Engine { private SCXML model; private int bootStrapMin; private List<CustomTagExtension> tagExtensionList; /** * Constructor */ public SCXMLEngine() { super(); tagExtensionList = new LinkedList<>(); tagExtensionList.add(new SetAssignExtension()); tagExtensionList.add(new SingleValueAssignExtension()); tagExtensionList.add(new FileExtension()); tagExtensionList.add(new RangeExtension()); ELEvaluator elEvaluator = new ELEvaluator(); ELContext context = new ELContext(); this.setEvaluator(elEvaluator); this.setRootContext(context); } /** * Alternative Constructor to support InLineTransformers within the model * * @param tagExtensionList the list of extensions to add */ public SCXMLEngine(final List<CustomTagExtension> tagExtensionList) { this(); // Adding all CustomTagExtensions - They will be added FIFO order // (Whatever CustomTagExtension that was added to the list first will be added to the engine first) for (CustomTagExtension cte:tagExtensionList) { addTagExtension(cte); } } /** * Searches the model for all variable assignments and makes a default map of those variables, setting them to "" * * @return the default variable assignment map */ private Map<String, String> fillInitialVariables() { Map<String, TransitionTarget> targets = model.getChildren(); Set<String> variables = new HashSet<>(); for (TransitionTarget target : targets.values()) { OnEntry entry = target.getOnEntry(); List<Action> actions = entry.getActions(); for (Action action : actions) { if (action instanceof Assign) { String variable = ((Assign) action).getName(); variables.add(variable); } else if (action instanceof SetAssignExtension.SetAssignTag) { String variable = ((SetAssignExtension.SetAssignTag) action).getName(); variables.add(variable); } } } Map<String, String> result = new HashMap<>(); for (String variable : variables) { result.put(variable, ""); } return result; } /** * Performs a partial BFS on model until the search frontier reaches the desired bootstrap size * * @param min the desired bootstrap size * @return a list of found PossibleState * @throws ModelException if the desired bootstrap can not be reached */ public List<PossibleState> bfs(int min) throws ModelException { List<PossibleState> bootStrap = new LinkedList<>(); TransitionTarget initial = model.getInitialTarget(); PossibleState initialState = new PossibleState(initial, fillInitialVariables()); bootStrap.add(initialState); while (bootStrap.size() < min) { PossibleState state = bootStrap.remove(0); TransitionTarget nextState = state.nextState; if (nextState.getId().equalsIgnoreCase("end")) { throw new ModelException("Could not achieve required bootstrap without reaching end state"); } //run every action in series List<Map<String, String>> product = new LinkedList<>(); product.add(new HashMap<>(state.variables)); OnEntry entry = nextState.getOnEntry(); List<Action> actions = entry.getActions(); for (Action action : actions) { for (CustomTagExtension tagExtension : tagExtensionList) { if (tagExtension.getTagActionClass().isInstance(action)) { product = tagExtension.pipelinePossibleStates(action, product); } } } //go through every transition and see which of the products are valid, adding them to the list List<Transition> transitions = nextState.getTransitionsList(); for (Transition transition : transitions) { String condition = transition.getCond(); TransitionTarget target = ((List<TransitionTarget>) transition.getTargets()).get(0); for (Map<String, String> p : product) { Boolean pass; if (condition == null) { pass = true; } else { //scrub the context clean so we may use it to evaluate transition conditional Context context = this.getRootContext(); context.reset(); //set up new context for (Map.Entry<String, String> e : p.entrySet()) { context.set(e.getKey(), e.getValue()); } //evaluate condition try { pass = (Boolean) this.getEvaluator().eval(context, condition); } catch (SCXMLExpressionException ex) { pass = false; } } //transition condition satisfied, add to bootstrap list if (pass) { PossibleState result = new PossibleState(target, p); bootStrap.add(result); } } } } return bootStrap; } /** * Performs the BFS and gives the results to a distributor to distribute * * @param distributor the distributor */ public void process(SearchDistributor distributor) { List<PossibleState> bootStrap; try { bootStrap = bfs(bootStrapMin); } catch (ModelException e) { bootStrap = new LinkedList<>(); } List<Frontier> frontiers = new LinkedList<>(); for (PossibleState p : bootStrap) { SCXMLFrontier dge = new SCXMLFrontier(p, model, tagExtensionList); frontiers.add(dge); } distributor.distribute(frontiers); } private List<CustomAction> customActionsFromTagExtensions() { List<CustomAction> customActions = new ArrayList<>(); for (CustomTagExtension tagExtension : tagExtensionList) { if (!tagExtension.getTagNameSpace().equals("http://www.w3.org/2005/07/scxml")) { CustomAction action = new CustomAction(tagExtension.getTagNameSpace(), tagExtension.getTagName(), tagExtension.getTagActionClass()); customActions.add(action); } } return customActions; } /** * Sets the SCXML model with an InputStream * * @param inputFileStream the model input stream */ public void setModelByInputFileStream(InputStream inputFileStream) { try { this.model = SCXMLParser.parse(new InputSource(inputFileStream), null, customActionsFromTagExtensions()); this.setStateMachine(this.model); } catch (IOException | SAXException | ModelException e) { e.printStackTrace(); } } /** * Sets the SCXML model with a string * * @param model the model text */ public void setModelByText(String model) { try { InputStream is = new ByteArrayInputStream(model.getBytes()); this.model = SCXMLParser.parse(new InputSource(is), null, customActionsFromTagExtensions()); this.setStateMachine(this.model); } catch (IOException | SAXException | ModelException e) { e.printStackTrace(); } } /** * bootstrapMin setter * * @param min sets the desired bootstrap min * @return this */ public Engine setBootstrapMin(int min) { bootStrapMin = min; return this; } /** * Adds a custom tag extension to this engine for use in model parsing and processing. Custom tags should be added * before the model is set. * * @param tagExtension the extension to add */ private void addTagExtension(CustomTagExtension tagExtension) { this.tagExtensionList.add(tagExtension); } }