All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.effektif.workflow.impl.WorkflowParser Maven / Gradle / Ivy

/*
 * 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 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 path;
  public ParseIssues issues;
  public Stack contextStack;
  public Set activityIds = new HashSet<>();
  public Set variableIds = new HashSet<>();
  public Set 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 workflowApi into workflowImpl and
   * adds any parse issues to workflowApi.
   * 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 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  List> parseBindings(List> bindings, String bindingName) {
    if (bindings==null) {
      return null;
    }
    List> bindingImpls = new ArrayList<>();
    for (Binding binding: bindings) {
      BindingImpl bindingImpl = parseBinding(binding, bindingName, false);
      bindingImpls.add(bindingImpl);
    }
    return bindingImpls;
  }

  public  BindingImpl parseBinding(Binding binding, String bindingName) {
    return parseBinding(binding, bindingName, false, null);
  }

  public  BindingImpl parseBinding(Binding 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  BindingImpl parseBinding(Binding binding, String bindingName, boolean isRequired, DataType type) {
    pushContext(bindingName, binding, null, null);
    BindingImpl 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  BindingImpl parseBinding(Binding binding, DataType targetType) {
    if (binding==null) {
      return null;
    }
    BindingImpl 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 getConfiguration(Class type) {
    return configuration.get(type);
  }

  public List getStartActivities(ScopeImpl scope) {
    if (scope.activities==null) {
      return null;
    }
    List 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) {
      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;
  }
}