/*
* Copyright 2014 Effektif GmbH.
*
* 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 com.effektif.workflow.impl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import com.effektif.workflow.api.condition.Unspecified;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.effektif.workflow.api.Configuration;
import com.effektif.workflow.api.condition.Condition;
import com.effektif.workflow.api.model.RelativeTime;
import com.effektif.workflow.api.model.WorkflowId;
import com.effektif.workflow.api.types.DataType;
import com.effektif.workflow.api.workflow.AbstractWorkflow;
import com.effektif.workflow.api.workflow.Activity;
import com.effektif.workflow.api.workflow.Binding;
import com.effektif.workflow.api.workflow.Element;
import com.effektif.workflow.api.workflow.ExecutableWorkflow;
import com.effektif.workflow.api.workflow.MultiInstance;
import com.effektif.workflow.api.workflow.ParseIssue.IssueType;
import com.effektif.workflow.api.workflow.ParseIssues;
import com.effektif.workflow.api.workflow.Transition;
import com.effektif.workflow.api.workflow.Variable;
import com.effektif.workflow.impl.conditions.ConditionImpl;
import com.effektif.workflow.impl.conditions.ConditionService;
import com.effektif.workflow.impl.configuration.Brewery;
import com.effektif.workflow.impl.data.DataTypeService;
import com.effektif.workflow.impl.job.RelativeTimeImpl;
import com.effektif.workflow.impl.template.Hint;
import com.effektif.workflow.impl.template.TextTemplate;
import com.effektif.workflow.impl.workflow.ActivityImpl;
import com.effektif.workflow.impl.workflow.BindingImpl;
import com.effektif.workflow.impl.workflow.ExpressionImpl;
import com.effektif.workflow.impl.workflow.MultiInstanceImpl;
import com.effektif.workflow.impl.workflow.ScopeImpl;
import com.effektif.workflow.impl.workflow.TransitionImpl;
import com.effektif.workflow.impl.workflow.WorkflowImpl;
/** Validates and wires process definition after it's been built by either the builder api or json deserialization. */
public class WorkflowParser {
public static final String PROPERTY_LINE = "line";
public static final String PROPERTY_COLUMN = "column";
public static final Logger log = LoggerFactory.getLogger(WorkflowParser.class);
public Configuration configuration;
public WorkflowImpl workflow;
public LinkedList<String> path;
public ParseIssues issues;
public Stack<ParseContext> contextStack;
public Set<String> activityIds = new HashSet<>();
public Set<String> variableIds = new HashSet<>();
public Set<String> transitionIds = new HashSet<>();
public WorkflowParseListener workflowParseListener;
public class ParseContext {
ParseContext(String property, Object element, Object elementImpl, Integer index) {
this.property = property;
this.element = element;
this.elementImpl = elementImpl;
String indexText = null;
if (element instanceof Element) {
indexText = getIdText(element);
}
if (indexText==null && index!=null) {
indexText = Integer.toString(index);
}
}
public Object element;
public String property;
public String index;
public Object elementImpl;
public String toString() {
if (index!=null) {
return property+"["+index+"]";
} else {
return property;
}
}
public Long getLine() {
if (element instanceof Element) {
Number line = (Number) ((Element)element).getProperty(PROPERTY_LINE);
return line!=null ? line.longValue() : null;
}
return null;
}
public Long getColumn() {
if (element instanceof Element) {
Number column = (Number) ((Element)element).getProperty(PROPERTY_COLUMN);
return column!=null ? column.longValue() : null;
}
return null;
}
}
public static String getIdText(Object object) {
if (object instanceof Activity) {
return ((Activity)object).getId();
} else if (object instanceof Transition) {
return ((Transition)object).getId();
} else if (object instanceof Variable) {
return ((Variable)object).getId();
} else if (object instanceof ExecutableWorkflow) {
WorkflowId workflowId = ((ExecutableWorkflow)object).getId();
return workflowId!=null ? workflowId.getInternal() : null;
}
return null;
}
public WorkflowParser(Configuration configuration) {
this.configuration = configuration;
this.path = new LinkedList<>();
this.contextStack = new Stack<>();
this.issues = new ParseIssues();
// this cast is necessary to get the workflow parse listener optional
// because the brewery.getOpt method is not available on the configuration itself
if (configuration instanceof DefaultConfiguration) {
DefaultConfiguration defaultConfiguration = (DefaultConfiguration)configuration;
Brewery brewery = defaultConfiguration.getBrewery();
this.workflowParseListener = brewery.getOpt(WorkflowParseListener.class);
}
}
/**
* Parses the content of <code>workflowApi</code> into <code>workflowImpl</code> and
* adds any parse issues to <code>workflowApi</code>.
* Use one parser for each parse.
*/
public WorkflowImpl parse(AbstractWorkflow workflowApi) {
workflow = new WorkflowImpl();
workflow.id = workflowApi.getId();
pushContext("workflow", workflowApi, workflow, null);
workflow.parse(workflowApi, this);
popContext();
if (this.workflowParseListener!=null) {
this.workflowParseListener.workflowParsed(workflowApi, workflow, this);
}
return workflow;
}
public void pushContext(String property, Object element, Object elementImpl, Integer index) {
this.contextStack.push(new ParseContext(property, element, elementImpl, index));
}
public void popContext() {
this.contextStack.pop();
}
protected String getPathText() {
StringBuilder pathText = new StringBuilder();
String dot = null;
for (ParseContext validationContext: contextStack) {
if (dot==null) {
dot = ".";
} else {
pathText.append(dot);
}
pathText.append(validationContext.toString());
}
return pathText.toString();
}
public String getExistingActivityIdsText(ScopeImpl scope) {
List<Object> activityIds = new ArrayList<>();
if (scope.activities!=null) {
for (ActivityImpl activity: scope.activities.values()) {
if (activity.id!=null) {
activityIds.add(activity.id);
}
}
}
return (!activityIds.isEmpty() ? "Should be one of "+activityIds : "No activities defined in this scope");
}
public <T> List<BindingImpl<T>> parseBindings(List<Binding<T>> bindings, String bindingName) {
if (bindings==null) {
return null;
}
List<BindingImpl<T>> bindingImpls = new ArrayList<>();
for (Binding<T> binding: bindings) {
BindingImpl<T> bindingImpl = parseBinding(binding, bindingName, false);
bindingImpls.add(bindingImpl);
}
return bindingImpls;
}
public <T> BindingImpl<T> parseBinding(Binding<T> binding, String bindingName) {
return parseBinding(binding, bindingName, false, null);
}
public <T> BindingImpl<T> parseBinding(Binding<T> binding, String bindingName, boolean isRequired) {
return parseBinding(binding, bindingName, isRequired, null);
}
/** @param type is only provided if the binding is untyped. in that case the jackson deserialization didn't
* instantiate the correct type and the deserialization needs to completed here based on the type.
* only provide the type if the binding is untyped, otherwise use null or {@link #parseBinding(Binding, String, boolean)}. */
public <T> BindingImpl<T> parseBinding(Binding<T> binding, String bindingName, boolean isRequired, DataType type) {
pushContext(bindingName, binding, null, null);
BindingImpl<T> bindingImpl = parseBinding(binding, type);
int values = 0;
if (bindingImpl!=null) {
if (bindingImpl.value!=null) values++;
if (bindingImpl.expression!=null) values++;
}
if (isRequired && values==0) {
addWarning("Binding '%s' required and not specified", bindingName);
} else if (values>1) {
addWarning("Multiple values specified for binding '%s'", bindingName);
}
popContext();
return bindingImpl;
}
protected <T> BindingImpl<T> parseBinding(Binding<T> binding, DataType targetType) {
if (binding==null) {
return null;
}
BindingImpl<T> bindingImpl = new BindingImpl<>();
if (binding.getValue()!=null) {
bindingImpl.value = binding.getValue();
DataTypeService ds = configuration.get(DataTypeService.class);
DataType type = binding.getType();
if (type==null && targetType!=null) {
type = targetType;
}
bindingImpl.type = ds.createDataType(type);
}
String expression = binding.getExpression();
if (expression!=null) {
bindingImpl.expression = new ExpressionImpl();
pushContext("expression", expression, bindingImpl.expression, null);
bindingImpl.expression.parse(expression, this);
popContext();
}
if (binding.getMetadata() != null) {
bindingImpl.metadata = binding.getMetadata();
}
String template = binding.getTemplate();
if (template!=null) {
bindingImpl.template = parseTextTemplate(template);
}
return bindingImpl;
}
public void addError(String message, Object... messageArgs) {
ParseContext currentContext = contextStack.peek();
issues.addIssue(IssueType.error, getPathText(), currentContext.getLine(), currentContext.getColumn(), message, messageArgs);
}
public void addWarning(String message, Object... messageArgs) {
ParseContext currentContext = contextStack.peek();
issues.addIssue(IssueType.warning, getPathText(), currentContext.getLine(), currentContext.getColumn(), message, messageArgs);
}
public ParseIssues getIssues() {
return issues;
}
public WorkflowParser checkNoErrors() {
issues.checkNoErrors();
return this;
}
public WorkflowParser checkNoErrorsAndNoWarnings() {
issues.checkNoErrorsAndNoWarnings();
return this;
}
public boolean hasErrors() {
return issues.hasErrors();
}
public WorkflowImpl getWorkflow() {
return workflow;
}
public <T> T getConfiguration(Class<T> type) {
return configuration.get(type);
}
public List<ActivityImpl> getStartActivities(ScopeImpl scope) {
if (scope.activities==null) {
return null;
}
List<ActivityImpl> startActivities = new ArrayList<>(scope.activities.values());
if (scope.transitions!=null) {
for (TransitionImpl transition: scope.transitions) {
startActivities.remove(transition.to);
}
}
if (startActivities.isEmpty()) {
this.addWarning("No start activities in %s", scope.getIdText());
}
return startActivities;
}
public MultiInstanceImpl parseMultiInstance(MultiInstance multiInstance) {
if (multiInstance==null) {
return null;
}
MultiInstanceImpl multiInstanceImpl = new MultiInstanceImpl();
multiInstanceImpl.parse(multiInstance, this);
return multiInstanceImpl;
}
public ConditionImpl parseCondition(Condition condition) {
if (condition==null || condition instanceof Unspecified) {
return null;
}
try {
return configuration
.get(ConditionService.class)
.compile(condition, this);
} catch (Exception e) {
addWarning("Invalid condition '%s' : %s", condition, e.getMessage());
}
return null;
}
public TextTemplate parseTextTemplate(String templateText, Hint... hints) {
if (templateText==null) {
return null;
}
return new TextTemplate(templateText, hints, this);
}
public RelativeTimeImpl parseRelativeTime(RelativeTime relativeTime) {
if (relativeTime==null || !relativeTime.valid()) {
return null;
}
return new RelativeTimeImpl(relativeTime, this);
}
public ScopeImpl getCurrentScope() {
for (int i=contextStack.size()-1; i>=0; i--) {
Object elementImpl = contextStack.get(i).elementImpl;
if (elementImpl instanceof ScopeImpl) {
return (ScopeImpl) elementImpl;
}
}
return null;
}
}