/*
* Copyright 2015 the original author or authors.
*
* 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.springframework.xd.dirt.job.dsl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.springframework.xd.dirt.stream.JobDefinitionRepository;
import org.springframework.xml.transform.StringResult;
/**
* The root AST node for any AST parsed from a job specification.
*
* Andy Clement
*/
public class JobSpecification extends AstNode {
/**
* The DSL text that was parsed to create this JobSpec.
*/
private String jobDefinitionText;
/**
* The top level JobNode within this JobSpec.
*/
private JobNode jobNode;
/**
* A list of jobs that were defined inline in the DSL text that was parsed
* to create this Ast. Computed on first reference.
*/
private List<JobDefinition> jobDefinitions;
/**
* Any arguments specified at the end of the DSL, e.g. --timeout
*/
private ArgumentNode[] globalOptions;
public JobSpecification(String jobDefinitionText, JobNode jobNode, ArgumentNode[] globalOptions) {
super(jobNode == null ? 0 : jobNode.getStartPos(), jobNode == null ? 0 : jobNode.getEndPos());
this.jobDefinitionText = jobDefinitionText;
this.jobNode = jobNode;
this.globalOptions = globalOptions;
}
public Map<String, String> getGlobalOptionsMap() {
if (globalOptions == null) {
return Collections.<String, String> emptyMap();
}
Map<String, String> optionsMap = new LinkedHashMap<String, String>();
for (ArgumentNode option : globalOptions) {
optionsMap.put(option.getName(), option.getValue());
}
return optionsMap;
}
@Override
public String stringify(boolean includePositionInfo) {
StringBuilder s = new StringBuilder();
s.append(jobNode.stringify(includePositionInfo));
if (globalOptions != null) {
for (ArgumentNode option : globalOptions) {
s.append(" ");
s.append(option.stringify(includePositionInfo));
}
}
return s.toString();
}
public String getJobDefinitionText() {
return jobDefinitionText;
}
public JobNode getJobNode() {
return this.jobNode;
}
/**
* A shortcut (avoiding traversing the tree) that returns the list
* of all job definitions inlined somewhere in this AST. Computed
* on demand.
*
* @return a list of inlined job definitions defined in this AST
*/
public List<JobDefinition> getJobDefinitions() {
if (jobDefinitions != null) {
return jobDefinitions;
}
JobDefinitionLocator jdl = new JobDefinitionLocator();
jdl.accept(this);
jobDefinitions = jdl.getJobDefinitions();
return jobDefinitions;
}
/**
* A shortcut (avoiding traversing the tree) that returns the list
* of all job references somewhere in this AST (references in
* transitions do not count).
*
* @return a list of job references in this AST
*/
public List<JobReference> getJobReferences() {
JobReferenceLocator jrl = new JobReferenceLocator();
jrl.accept(this);
return jrl.getJobReferences();
}
/**
* Performs validation of the AST. Where the initial parse is about
* checking the syntactic structure, validation is about checking more
* semantic elements:<ul>
* <li>Do the inline job definitions refer to valid job modules?
* <li>Do the inline job definitions supply correct arguments?
* <li>Do the job references point to valid job definitions?
* </ul>
* @param jobDefinitionRepository a repository to check job definitions against
* @throws JobSpecificationException if validation fails
*/
public void validate(JobDefinitionRepository jobDefinitionRepository) {
// TODO validate the job references (this will be done at deploy time but we could do it earlier)
}
/**
* @return this AST converted to a Graph form for display by Flo
*/
public Graph toGraph() {
GraphGeneratorVisitor ggv = new GraphGeneratorVisitor();
ggv.accept(this);
return ggv.getGraph();
}
/**
* @param batchJobId the id that will be inserted into the XML document for the batch:job element
* @return this AST converted to an XML form
*/
public String toXML(String batchJobId) {
return toXML(batchJobId, false);
}
/**
* @param batchJobId the id that will be inserted into the XML document for the batch:job element
* @param prettyPrint determine if the XML should be human readable.
* @return this AST converted to an XML form
*/
public String toXML(String batchJobId, boolean prettyPrint) {
XMLGeneratorVisitor xgv = new XMLGeneratorVisitor(batchJobId, prettyPrint);
xgv.accept(this);
return xgv.getXmlString();
}
/**
* Basic visitor that simply collects up any inlined job definitions.
*/
static class JobDefinitionLocator extends JobSpecificationVisitor<Object> {
List<JobDefinition> jobDefinitions = new ArrayList<JobDefinition>();
public List<JobDefinition> getJobDefinitions() {
return jobDefinitions;
}
@Override
public Object walk(Object context, Flow sjs) {
for (JobNode jobNode : sjs.getSeries()) {
walk(context, jobNode);
}
return context;
}
@Override
public Object walk(Object context, JobDefinition jd) {
jobDefinitions.add(jd);
return context;
}
@Override
public Object walk(Object context, JobReference jr) {
return context;
}
@Override
public Object walk(Object context, Split pjs) {
for (JobNode jobNode : pjs.getSeries()) {
walk(context, jobNode);
}
return context;
}
}
/**
* Basic visitor that simply collects up any job references (*not* those named in transitions)
*/
static class JobReferenceLocator extends JobSpecificationVisitor<Object> {
List<JobReference> jobReferences = new ArrayList<JobReference>();
public List<JobReference> getJobReferences() {
return jobReferences;
}
@Override
public Object walk(Object context, Flow sjs) {
for (JobNode jobNode : sjs.getSeries()) {
walk(context, jobNode);
}
return context;
}
@Override
public Object walk(Object context, JobDefinition jd) {
return context;
}
@Override
public Object walk(Object context, JobReference jr) {
jobReferences.add(jr);
return context;
}
@Override
public Object walk(Object context, Split pjs) {
for (JobNode jobNode : pjs.getSeries()) {
walk(context, jobNode);
}
return context;
}
}
/**
* Visitor that produces an XML representation of the Job specification.
*/
static class XMLGeneratorVisitor extends JobSpecificationVisitor<Element[]> {
/**
* containing document that can be used for element creation
*/
private Document doc;
/**
* Where to append elements created during the visit
*/
private Element batchJobElement;
/**
* Should the XML output be readable or compressed onto one line.
*/
private boolean prettyPrint;
/**
* A stack that tracks the element that should have new children attached to it.
* Initially populated with batchJobElement from above.
*/
private Stack<Element> currentElement = new Stack<>();
/**
* Counter for the numeric suffix to attach to generated split id attributes.
*/
private int splitIdCounter = 1;
private List<String> jobRunnerBeanNames = new ArrayList<>();
// As a new flow is entered, a new map is pushed here (popped on flow exit).
// The map holds onto a map of transition names to allocated XML IDs within that flow.
// This ensures all references to the same transition in a flow point to the same XML ID
// But in a different flow the same transition names will point to a different XML ID.
private Stack<Map<String, String>> transitionNamesToElementIdsInFlow = new Stack<>();
// Knowing all the explicit job references in the tree means when seeing a transition
// it can be determined if it is to a node not yet visited or something that will
// never be visited (and so the step must be created right now).
private Map<JobReference, String> jobReferencesToElementIds = new LinkedHashMap<>();
private String xmlString;
private String batchJobId;
XMLGeneratorVisitor(String batchJobId, boolean prettyPrint) {
this.batchJobId = batchJobId;
this.prettyPrint = prettyPrint;
}
@Override
protected void accept(JobSpecification jobSpec) {
List<JobReference> jobReferences = jobSpec.getJobReferences();
for (JobReference jr : jobReferences) {
// Allocate unique XML Element IDs now, makes life easier later
jobReferencesToElementIds.put(jr, getNextStepId(jr.getName()));
}
super.accept(jobSpec);
}
public String getXmlString() {
return xmlString;
}
@Override
public Element[] preJobSpecWalk() {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
DOMImplementation domImplementation = db.getDOMImplementation();
// Generate:
// <beans xmlns="http://www.springframework.org/schema/beans"
// xmlns:batch="http://www.springframework.org/schema/batch"
// xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
// xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
// http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd">
this.doc = domImplementation.createDocument("http://www.springframework.org/schema/beans", "beans",
null);
doc.createElementNS("http://www.springframework.org/schema/batch", "batch");
doc.getDocumentElement().setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:batch",
"http://www.springframework.org/schema/batch");
doc.getDocumentElement().setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xsi",
"http://www.w3.org/2001/XMLSchema-instance");
doc.getDocumentElement().setAttributeNS(
"http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation",
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd "
+
"http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd");
// Setting this 'again' to get it on the front and look more like the above.
doc.getDocumentElement().setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns",
"http://www.springframework.org/schema/beans");
// Generate: <bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
Element taskExecutor = doc.createElement("bean");
doc.getDocumentElement().appendChild(taskExecutor);
taskExecutor.setAttribute("id", "taskExecutor");
taskExecutor.setAttribute("class", "org.springframework.core.task.SimpleAsyncTaskExecutor");
// Generate: <batch:job id="streamName" xmlns="http://www.springframework.org/schema/batch">
this.batchJobElement = doc.createElement("batch:job");
doc.getDocumentElement().appendChild(batchJobElement);
batchJobElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns",
"http://www.springframework.org/schema/batch");
if (batchJobId != null) {
batchJobElement.setAttribute("id", batchJobId);
}
transitionNamesToElementIdsInFlow.push(new LinkedHashMap<String, String>());
this.currentElement.push(batchJobElement);
}
catch (Exception e) {
throw new IllegalStateException("Unexpected problem building XML representation", e);
}
return null;
};
/**
* Determine if a step element has any <next.../> elements. If it does then
* it is mapping the exit space from this step.
* @param step the XML element representing the step
* @return true if the supplied step specifies any transitions via next elements
*/
private boolean isMappingExitSpace(Element step) {
NodeList children = step.getChildNodes();
for (int c = 0; c < children.getLength(); c++) {
String nodeName = children.item(c).getNodeName();
if (nodeName.equals("next") || nodeName.equals("end") || nodeName.equals("fail")) {
return true;
}
}
return false;
}
/**
* Check if the XML element for a step indicates a <next.../> element for a specific
* exit status.
* @param step the XML element representing the step
* @param exitStatus the exit status to check for
* @return true if the step specifies a next attribute for the supplied exit status
*/
private boolean isMappingExitStatus(Element step, String exitStatus) {
NodeList children = step.getChildNodes();
for (int c = 0; c < children.getLength(); c++) {
String nodeName = children.item(c).getNodeName();
if (nodeName.equals("next") || nodeName.equals("end") || nodeName.equals("fail")) {
String onAttributeValue = children.item(c).getAttributes().getNamedItem("on").getNodeValue();
if (onAttributeValue.equals(exitStatus)) {
return true;
}
}
}
return false;
}
/**
* Create a <next on= to=> attribute and attach it to the supplied step element.
* @param step the step that will get the new attribute
* @param jobExitStatus the jobExitStatus to fill in as the 'on' attribute value field
* @param targetId the target XML ID to fill in as the 'to' attribute value field
*/
private void addNextAttribute(Element step, String jobExitStatus, String targetId) {
Element next = doc.createElement("next");
next.setAttribute("on", jobExitStatus);
next.setAttribute("to", targetId);
step.appendChild(next);
}
/**
* Create a <fail on=> attribute and attach it to the supplied step element.
* @param step the step that will get the new attribute
* @param jobExitStatus the jobExitStatus to fill in as the 'on' attribute value field
*/
private void addFailAttribute(Element step, String jobExitStatus) {
Element fail = doc.createElement("fail");
fail.setAttribute("on", jobExitStatus);
step.appendChild(fail);
}
/**
* Create a <end on=> attribute and attach it to the supplied step element.
* @param step the step that will get the new attribute
* @param jobExitStatus the jobExitStatus to fill in as the 'on' attribute value field
*/
private void addEndAttribute(Element step, String jobExitStatus) {
Element fail = doc.createElement("end");
fail.setAttribute("on", jobExitStatus);
step.appendChild(fail);
}
@Override
public void postJobSpecWalk(Element[] elements, JobSpecification jobSpec) {
if (elements != null) {
// These are the final elements that were visited
for (Element element : elements) {
if (isMappingExitSpace(element)) {
if (!isMappingExitStatus(element, "*")) {
addFailAttribute(element, "*");
}
}
}
}
Map<String, String> transitionStepsToCreate = transitionNamesToElementIdsInFlow.pop();
for (Map.Entry<String, String> transitionStepToCreate : transitionStepsToCreate.entrySet()) {
Element step = createStep(transitionStepToCreate.getValue(), transitionStepToCreate.getKey());
currentElement.peek().appendChild(step);
}
Set<String> generatedBeans = new HashSet<>();
for (String jobRunnerBeanName : jobRunnerBeanNames) {
if (generatedBeans.contains(jobRunnerBeanName)) {
continue;
}
// Producing:
// <bean class="org.springframework.xd.dirt.batch.tasklet.JobLaunchingTasklet" id="jobRunner-bbb" scope="step">
// <constructor-arg ref="messageBus"/>
// <constructor-arg ref="jobDefinitionRepository"/>
// <constructor-arg ref="xdJobRepository"/>
// <constructor-arg value="bbb"/>
// <constructor-arg value="${timeout}"/>
// </bean>
Element bean = doc.createElement("bean");
bean.setAttribute("scope", "step");
bean.setAttribute("class", "org.springframework.xd.dirt.batch.tasklet.JobLaunchingTasklet");
bean.setAttribute("id", "jobRunner-" + jobRunnerBeanName);
addConstructorArg(bean, "ref", "messageBus");
addConstructorArg(bean, "ref", "jobDefinitionRepository");
addConstructorArg(bean, "ref", "xdJobRepository");
addConstructorArg(bean, "value", jobRunnerBeanName);
addConstructorArg(bean, "value", "${timeout}");
this.doc.getElementsByTagName("beans").item(0).appendChild(bean);
generatedBeans.add(jobRunnerBeanName);
}
try {
// Write the content
TransformerFactory transformerFactory = TransformerFactory.newInstance();
javax.xml.transform.Transformer transformer;
transformer = transformerFactory.newTransformer();
if (prettyPrint) {
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
}
DOMSource source = new DOMSource(doc);
StringResult sr = new StringResult();
transformer.transform(source, sr);
xmlString = sr.toString().trim();
}
catch (TransformerException e) {
throw new IllegalStateException("Unexpected problem building XML representation", e);
}
}
private void addConstructorArg(Element bean, String attributeName, String argName) {
Element ctorArgElement = doc.createElement("constructor-arg");
ctorArgElement.setAttribute(attributeName, argName);
bean.appendChild(ctorArgElement);
}
@Override
public Element[] walk(Element[] context, Flow jn) {
boolean inSplit = currentElement.peek().getTagName().equals("split");
// Only need the flow element if nested in a split
if (inSplit) {
Element flow = doc.createElement("flow");
currentElement.peek().appendChild(flow);
currentElement.push(flow);
}
Element[] result = context;
transitionNamesToElementIdsInFlow.push(new LinkedHashMap<String, String>());
for (JobNode j : jn.getSeries()) {
result = walk(result, j);
}
Map<String, String> transitionStepsToCreate = transitionNamesToElementIdsInFlow.pop();
for (Map.Entry<String, String> transitionStepToCreate : transitionStepsToCreate.entrySet()) {
Element step = createStep(transitionStepToCreate.getValue(), transitionStepToCreate.getKey());
currentElement.peek().appendChild(step);
}
if (inSplit) {
currentElement.pop();
}
return result;
}
@Override
public Element[] walk(Element[] context, JobDefinition jd) {
// TODO this code needs some rework to match XML gen for JobReference but we don't
// support JobDefinitions in the first version of the DSL.
Element step = doc.createElement("step");
step.setAttribute("id", jd.getJobName());
Element tasklet = doc.createElement("tasklet");
String jobRunnerId = "jobRunner-" + jd.getJobName();
tasklet.setAttribute("ref", jobRunnerId);
jobRunnerBeanNames.add(jd.getName());
step.appendChild(tasklet);
Element next = null;
if (jd.hasTransitions()) {
for (Transition t : jd.transitions) {
if (t.getTargetJobName().equals(Transition.FAIL)) {
addFailAttribute(step, t.getStateName());
}
else if (t.getTargetJobName().equals(Transition.END)) {
addEndAttribute(step, t.getStateName());
}
else {
addNextAttribute(step, t.getStateName(), t.getTargetJobName());
}
}
// If there are transitions, it is necessary to ensure the whole exit space is covered from this
}
if (context != null) {
// context is an array of earlier elements that should point to this one
Element[] elements = context;
for (Element element : elements) {
next = doc.createElement("next");
next.setAttribute("on", "COMPLETED");
next.setAttribute("to", jd.getJobName());
element.appendChild(next);
}
}
this.currentElement.peek().appendChild(step);
return new Element[] { step };
}
private List<String> allocatedStepIds = new ArrayList<>();
/**
* Determine the next unique ID we can use for an XML element.
*/
private String getNextStepId(String prefix) {
if (!allocatedStepIds.contains(prefix)) {
// Avoid number suffix for the first one
allocatedStepIds.add(prefix);
return prefix;
}
int suffix = 1;
String proposal = null;
do {
proposal = new StringBuilder(prefix).append(Integer.toString(suffix++)).toString();
}
while (allocatedStepIds.contains(proposal));
allocatedStepIds.add(proposal);
return proposal;
}
/**
* Visit a job reference. Rules:
* <ul>
* <li>The flow surrounding element for the step is created if inside a split
* </ul>
*/
@Override
public Element[] walk(Element[] context, JobReference jr) {
// Producing this kind of construct:
// <flow">
// <step id="sqoop-6e44">
// <tasklet ref="jobRunner-6e44"/>
// <next on="COMPLETED" to="sqoop-e07a"/>
// <next on="FAILED" to="kill1"/>
// <fail on="*"/>
// </step>
// </flow>
// When a split branch only contains a single job reference, no surrounding Flow object is created,
// so the flow block needs creating here in this case.
boolean inSplit = currentElement.peek().getTagName().equals("split");
if (inSplit) {
Element flow = doc.createElement("flow");
currentElement.peek().appendChild(flow);
currentElement.push(flow);
}
String stepId = jobReferencesToElementIds.get(jr);
Element step = createStep(stepId, jr.getName());
currentElement.peek().appendChild(step);
jobRunnerBeanNames.add(jr.getName());
boolean explicitWildcardExit = false;
if (jr.hasTransitions()) {
for (Transition t : jr.transitions) {
if (t.getStateName().equals("*")) {
explicitWildcardExit = true;
}
String targetJob = t.getTargetJobName();
if (targetJob.equals(Transition.END)) {
addEndAttribute(step, t.getStateName());
continue;
}
else if (targetJob.equals(Transition.FAIL)) {
addFailAttribute(step, t.getStateName());
continue;
}
Map<String, String> transitionNamesToElementIdsInCurrentFlow = transitionNamesToElementIdsInFlow.peek();
if (transitionNamesToElementIdsInCurrentFlow.containsKey(targetJob)) {
// already exists, share the ID
targetJob = transitionNamesToElementIdsInCurrentFlow.get(targetJob);
}
else {
// Is this a reference to a job that already exists elsewhere in this composed job definition?
String id = getReferenceToExistingJob(targetJob);
if (id == null) {
// create an entry, this is the first reference to this target job in this flow
id = getNextStepId(targetJob);
transitionNamesToElementIdsInCurrentFlow.put(targetJob, id);
if (inSplit) {
// If a job reference is directly inside a split with no surrounding flow to create
// the steps collected in 'existingTransitionSteps' then it needs to be done here.
Element transitionStep = createStep(id, t.getTargetJobName());
currentElement.peek().appendChild(transitionStep);
}
}
targetJob = id;
}
addNextAttribute(step, t.getStateName(), targetJob);
jobRunnerBeanNames.add(t.getTargetJobName());
}
if (inSplit) {
// The split is the element that will be analyzed to see if all exit
// statuses are covered. So for a job reference created here we need to
// ensure we do the analysis here.
if (!isMappingExitStatus(step, "*")) {
addFailAttribute(step, "*");
}
}
}
if (context != null) {
// context is an array of earlier elements that should be updated now to point to this one
for (Element element : context) {
addNextAttribute(element, "COMPLETED", stepId);
if (!isMappingExitStatus(element, "*")) {
addFailAttribute(element, "*");
}
}
}
if (inSplit) {
currentElement.pop();
}
return explicitWildcardExit ? new Element[] {} : new Element[] { step };
}
@Override
public Element[] walk(Element[] context, Split pjs) {
// Producing this kind of output:
// <split id="split1" task-executor="taskExecutor">
// ...
// </split>
Element split = doc.createElement("split");
String splitId = "split" + (splitIdCounter++);
split.setAttribute("task-executor", "taskExecutor");
split.setAttribute("id", splitId);
if (context != null) {
// context is an array of earlier elements that should point to this one
for (Element element : context) {
addNextAttribute(element, "COMPLETED", splitId);
if (!isMappingExitStatus(element, "*")) {
addFailAttribute(element, "*");
}
}
}
currentElement.peek().appendChild(split);
currentElement.push(split);
Element[] inputContext = new Element[] {};//context;
Element[] result = new Element[0];
for (JobNode jn : pjs.getSeries()) {
transitionNamesToElementIdsInFlow.push(new LinkedHashMap<String, String>());
Object outputContext = walk(inputContext, jn);
transitionNamesToElementIdsInFlow.pop();
result = merge(result, outputContext);
}
currentElement.pop();
// The only element from here to connect to the 'next thing' is the split node.
// This means only the split gets a 'next on="*"' element.
return new Element[] { split };
}
private Element[] merge(Element[] input, Object additional) {
Element[] additionalArrayData = (Element[]) additional;
Element[] result = new Element[input.length + additionalArrayData.length];
System.arraycopy(input, 0, result, 0, input.length);
System.arraycopy(additionalArrayData, 0, result, input.length, additionalArrayData.length);
return result;
}
private Element createStep(String stepId, String jobRunnerBeanIdSuffix) {
Element step = doc.createElement("step");
step.setAttribute("id", stepId);
Element tasklet = doc.createElement("tasklet");
tasklet.setAttribute("ref", "jobRunner-" + jobRunnerBeanIdSuffix);
step.appendChild(tasklet);
return step;
}
private String getReferenceToExistingJob(String jobName) {
for (Map.Entry<JobReference, String> jrEntry : jobReferencesToElementIds.entrySet()) {
if (jrEntry.getKey().getName().equals(jobName)) {
return jrEntry.getValue();
}
}
return null;
}
}
/**
* Visitor that produces a Graph representation of the Job specification suitable
* for display by Flo.
*/
static class GraphGeneratorVisitor extends JobSpecificationVisitor<int[]> {
private int id = 0;
private Map<String, Node> createdNodes = new HashMap<>();
// As the visit proceeds different contexts are entered/left - into a flow, into a split, etc.
// The context object on top of the stack allows a particular visit method
// to know what the current context is.
private Stack<Context> contexts = new Stack<>();
/**
* Encapsulates the current context
*/
static class Context {
// Set when processing the last element of a flow
boolean isEndOfFlow = false;
List<Integer> flowExits;
Map<String, Node> nodesSharedInFlow = new LinkedHashMap<String, Node>();
// Set whilst in a flow, allows us to recognize forward references from
// transitions (references to jobs named explicitly later in the flow)
List<String> jobsInFlow;
Context(boolean inFlow) {
if (inFlow) {
flowExits = new ArrayList<Integer>();
}
}
public void setJobsInFlow(List<String> jobsNamedInFlow) {
jobsInFlow = jobsNamedInFlow;
}
}
// The constructed graph elements (nodes, links, properties):
private List<Node> nodes = new ArrayList<>();
private List<Link> links = new ArrayList<>();
private Map<String, String> properties = new LinkedHashMap<String, String>();
public Graph getGraph() {
Graph g = new Graph(nodes, links, properties);
return g;
}
@Override
public int[] preJobSpecWalk() {
// Insert a START node at the beginning of the graph
Node node = new Node(Integer.toString(id++), "START");
nodes.add(node);
contexts.push(new Context(false));
return new int[] { 0 };
}
@Override
public void postJobSpecWalk(int[] finalNodes, JobSpecification jobSpec) {
// Insert an END node
int endId = id++;
Node endNode = new Node(Integer.toString(endId), "END");
nodes.add(endNode);
contexts.pop();
// Handle special case where $END has been used. For example:
// foo | oranges=$END
// <foo | oranges = $END & xx>
// In both these cases it looks odd if the graph shows a line between two $ENDs, so
// they are collapsed together and the links from the former $END are forwarded to the
// final $END. (The former $END node is then deleted).
for (int i : finalNodes) {
// Is the incoming node a $END node?
Node incomingNode = findNodeById(i);
if (incomingNode.name.equals("END")) {
List<Link> linksToUpdate = getLinksTo(i);
for (Link link : linksToUpdate) {
link.updateTo(Integer.toString(endId));
}
// Everything to this END node has been re-routed, delete it now
nodes.remove(incomingNode);
}
else {
// Just link the final node to the real $END
links.add(new Link(i, endId));
}
}
Map<String, String> options = jobSpec.getGlobalOptionsMap();
if (options.size() != 0) {
for (Map.Entry<String, String> option : options.entrySet()) {
properties.put(option.getKey(), option.getValue());
}
}
}
@Override
public int[] walk(int[] context, Flow jn) {
int[] result = context;
contexts.push(new Context(true));
// Compute the jobs in this flow to cope with
// forward references
List<String> jobsNamedInFlow = new ArrayList<String>();
for (JobNode j : jn.getSeries()) {
if (j instanceof JobDescriptor) {
jobsNamedInFlow.add(((JobDescriptor) j).getName());
}
}
contexts.peek().setJobsInFlow(jobsNamedInFlow);
Iterator<JobNode> seriesIterator = jn.getSeries().iterator();
while (seriesIterator.hasNext()) {
JobNode j = seriesIterator.next();
if (!seriesIterator.hasNext()) {
// For the last element in the series, set this flag
contexts.peek().isEndOfFlow = true;
}
result = walk(result, j);
}
// Merge the outputs from the last node in the flow
// with any transitional exits from the flow
List<Integer> exits = contexts.pop().flowExits;
for (int r : result) {
exits.add(r);
}
result = toIntArray(exits);
return result;
}
@Override
public int[] walk(int[] context, Split pjs) {
int[] inputContext = context;
if (inputContext.length > 1) {
// Cannot directly connect a split to a split, we need a SYNC node
// to simulate fan-in/fan-out (inputContext.length > 1 indicates a
// previous split).
int nextId = id++;
Node node = new Node(Integer.toString(nextId), "SYNC");
nodes.add(node);
for (int i : inputContext) {
Link l = new Link(i, nextId);
links.add(l);
}
// Now create new context that contains only the sync node output
inputContext = new int[] { nextId };
}
int[] result = new int[0];
for (JobNode jn : pjs.getSeries()) {
contexts.push(new Context(false));
Object outputContext = walk(inputContext, jn);
contexts.pop();
result = merge(result, outputContext);
}
return result;
}
@Override
public int[] walk(int[] context, JobReference jr) {
Map<String, String> properties = null;
ArgumentNode[] args = jr.getArguments();
if (args != null && args.length != 0) {
properties = new LinkedHashMap<>();
for (ArgumentNode arg : args) {
properties.put(arg.getName(), arg.getValue());
}
}
Node node;
int nextId;
// Check if this walk has hit something created earlier in this flow
// which may have occurred if a transition referred to it, e.g.
// foo | fail=bar || goo || bar
// where 'walk'ing foo will have created a node for bar - reuse it.
Node existingNode = contexts.peek().nodesSharedInFlow.get(jr.getName());
if (existingNode != null) {
// Found it, reuse it here
node = existingNode;
nextId = Integer.parseInt(existingNode.id);
}
else {
// Not already in this flow, create a new one
nextId = id++;
node = new Node(Integer.toString(nextId), jr.getName(), null, properties);
nodes.add(node);
}
createdNodes.put(jr.getName(), node);
// Should nodes created in the mainline visit be inserted into the shared set?
// (In addition to those created for transitions). Do we need it to support
// back references (loops?)
// Create links from the previous nodes to this one
if (context != null) {
int[] s = context;
for (int i : s) {
Link l = new Link(i, nextId);
links.add(l);
}
}
// Process all the transitions
boolean explicitWildcardUsed = false; // Is '*' used in any transition?
boolean isMappingExitSpace = false; // Are they mapping the exit space (i.e. have transitions)
// This list will collect transitional exits and the 'usual' COMPLETED exit if it
// applies (i.e. they haven't used transitions that prevent it applying)
List<Integer> allExitsToReturn = new ArrayList<>();
if (jr.hasTransitions()) {
isMappingExitSpace = true;
List<Integer> flowExits = contexts.peek().flowExits;
for (Transition t : jr.getTransitions()) {
if (t.getStateNameInDSLForm().equals("'*'")) {
explicitWildcardUsed = true;
}
String jobExitStatus = t.getStateNameInDSLForm();
String targetJobName = t.getTargetJobName();
int transitionTargetId = buildTransition(nextId, jobExitStatus, targetJobName);
// If something was created and it isn't a node further down the flow, record the exit
if (transitionTargetId != -1 && !isForwardReferenceInFlow(targetJobName)) {
if (flowExits != null) {
flowExits.add(transitionTargetId);
}
else {
allExitsToReturn.add(transitionTargetId);
}
}
}
}
// Determine if this node should be linked to following nodes.
if (isMappingExitSpace) {
// Only automatic if not the last one in the flow
// The latter condition here is checking if this is a jobreference visit
// immediately inside a split (with no surrounding flow) which should
// be treated the same as the end of a flow
if (!explicitWildcardUsed) {
if ((!contexts.peek().isEndOfFlow && contexts.peek().flowExits != null)) {
allExitsToReturn.add(nextId);
}
}
}
else {
allExitsToReturn.add(nextId);
}
return toIntArray(allExitsToReturn);
}
int[] merge(int[] input, Object additional) {
int[] additionalArrayData = (int[]) additional;
int[] result = new int[input.length + additionalArrayData.length];
System.arraycopy(input, 0, result, 0, input.length);
System.arraycopy(additionalArrayData, 0, result, input.length, additionalArrayData.length);
return result;
}
public Node findNodeInList(List<Integer> nodesInScope, String name) {
if (nodesInScope != null) {
for (Integer i : nodesInScope) {
for (Node n : nodes) {
if (n.id.equals(i)) {
// This one is in scope
if (n.name.equals(name)) {
return n;
}
}
}
}
}
return null;
}
/**
* Check if the specified job name refers to a job mentioned later
* in this flow.
* @param targetJobName the job name to search for
* @return true if it is a reference to a job later in the flow
*/
private boolean isForwardReferenceInFlow(String searchJobName) {
List<String> jobsInFlow = contexts.peek().jobsInFlow;
if (jobsInFlow != null) {
for (String jobname : jobsInFlow) {
if (jobname.equals(searchJobName)) {
return true;
}
}
}
return false;
}
/**
* Construct a transition link, and possibly a new node if a suitable candidate does not already exist.
* If a transition has already referenced a particular job in the same flow, reuse it. If not then
* create a new node and link to it.
*
* @param fromJobNodeId the id of the jobnode from which the transition is occurring
* @param jobExitStatus the job exit status attached to this transition
* @param targetJobName the transition target
* @return the ID of the transition target node if a new node created, otherwise -1
*/
private int buildTransition(int fromJobNodeId, String jobExitStatus, String targetJobName) {
boolean createdNewTarget = false;
if (targetJobName.equals(Transition.FAIL)) {
targetJobName = "FAIL";
}
else if (targetJobName.equals(Transition.END)) {
targetJobName = "END";
}
Node targetNode = contexts.peek().nodesSharedInFlow.get(targetJobName);//scope.peek().get(targetJobName);
int transitionTargetId;
if (targetNode == null) {
transitionTargetId = id++;
targetNode = new Node(Integer.toString(transitionTargetId), targetJobName);
nodes.add(targetNode);
createdNewTarget = true;
}
else {
transitionTargetId = Integer.parseInt(targetNode.id);
}
links.add(new Link(fromJobNodeId, transitionTargetId, jobExitStatus));
//scope.peek().put(targetJobName, targetNode);
contexts.peek().nodesSharedInFlow.put(targetJobName, targetNode);
return createdNewTarget ? transitionTargetId : -1;
}
@Override
public int[] walk(int[] context, JobDefinition jd) {
// Inline job definitions not yet supported
// int nextId = id++;
// Map<String, String> properties = null;
// ArgumentNode[] args = jd.getArguments();
// if (args != null && args.length != 0) {
// properties = new LinkedHashMap<>();
// for (ArgumentNode arg : args) {
// properties.put(arg.getName(), arg.getValue());
// }
// }
// Map<String, String> metadata = new HashMap<>();
// metadata.put(Node.METADATAKEY_JOBMODULENAME, jd.getJobModuleName());
// Node node = new Node(Integer.toString(nextId), jd.getJobName(), metadata, properties);
// nodes.add(node);
// createdNodes.put(node.name, node);
// if (context != null) {
// int[] s = context;
// for (int i : s) {
// Link l = new Link(i, nextId);
// links.add(l);
// }
// }
// if (jd.hasTransitions()) {
// for (Transition t : jd.getTransitions()) {
// transitions.add(new TransitionToMap(nextId, t.getStateNameInDSLForm(), t.getTargetJobName()));
// }
// }
// return new int[] { nextId };
return new int[] {};
}
private List<Link> getLinksTo(int i) {
List<Link> result = new ArrayList<>();
for (Link link : links) {
if (i == Integer.parseInt(link.to)) {
result.add(link);
}
}
return result;
}
private Node findNodeById(int i) {
for (Node node : nodes) {
if (Integer.parseInt(node.id) == i) {
return node;
}
}
return null;
}
private int[] toIntArray(List<Integer> integers) {
int[] result = new int[integers.size()];
for (int i = 0; i < integers.size(); i++) {
result[i] = integers.get(i);
}
return result;
}
}
public JobDefinition getJobDefinition(String jobName) {
for (JobDefinition jd : getJobDefinitions()) {
if (jd.getJobName().equals(jobName)) {
return jd;
}
}
return null;
}
/**
* Pretty print the text for this job specification, including appropriate
* newlines and indentation.
* @return formatted job specification.
*/
public String format() {
return jobNode.format(0);
}
}