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

io.resys.hdes.client.spi.flow.FlowProgramExecutor Maven / Gradle / Ivy

There is a newer version: 3.130.78
Show newest version
package io.resys.hdes.client.spi.flow;

/*-
 * #%L
 * hdes-client-api
 * %%
 * Copyright (C) 2020 - 2021 Copyright 2020 ReSys OÜ
 * %%
 * 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.
 * #L%
 */

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.resys.hdes.client.api.HdesClient.HdesTypesMapper;
import io.resys.hdes.client.api.exceptions.ProgramException;
import io.resys.hdes.client.api.programs.FlowProgram;
import io.resys.hdes.client.api.programs.FlowProgram.FlowExecutionStatus;
import io.resys.hdes.client.api.programs.FlowProgram.FlowProgramStep;
import io.resys.hdes.client.api.programs.FlowProgram.FlowProgramStepThenPointer;
import io.resys.hdes.client.api.programs.FlowProgram.FlowProgramStepWhenThenPointer;
import io.resys.hdes.client.api.programs.FlowProgram.FlowResult;
import io.resys.hdes.client.api.programs.FlowProgram.FlowResultErrorLog;
import io.resys.hdes.client.api.programs.FlowProgram.FlowResultLog;
import io.resys.hdes.client.api.programs.ImmutableFlowExecutionLog;
import io.resys.hdes.client.api.programs.ImmutableFlowResult;
import io.resys.hdes.client.api.programs.ImmutableFlowResultErrorLog;
import io.resys.hdes.client.api.programs.ImmutableFlowResultLog;
import io.resys.hdes.client.api.programs.Program.ProgramContext;
import io.resys.hdes.client.api.programs.Program.ProgramContextNamedValue;
import io.resys.hdes.client.spi.ImmutableProgramContext;
import io.resys.hdes.client.spi.decision.DecisionProgramExecutor;
import io.resys.hdes.client.spi.expression.OperationFlowContext.FlowTaskExpressionContext;
import io.resys.hdes.client.spi.groovy.ServiceProgramExecutor;

public class FlowProgramExecutor {
  private static final Logger LOGGER = LoggerFactory.getLogger(FlowProgramExecutor.class);
  private final HdesTypesMapper factory;
  private final FlowProgram program;
  private final ProgramContext context;
  private final Map accepted = new HashMap<>();
  private final Map stepLogs = new HashMap<>();
  private final LocalDateTime start = LocalDateTime.now();
  private final StringBuilder shortHistory = new StringBuilder();
  private final FlowTaskExpressionContext expressionContext;
  
  public FlowProgramExecutor(FlowProgram program, ProgramContext context, HdesTypesMapper factory) {
    super();
    this.program = program;
    this.context = context;
    this.factory = factory;
    this.expressionContext = new FlowTaskExpressionContext() {
      @Override
      public Object apply(String name) {
        return visitVariableOnPath(name);
      }
    };
  }

  public FlowResult run() {
    
    accepted.putAll(visitAcceptedDef(program, context));
    
    FlowResultLog last;
    FlowExecutionStatus status = FlowExecutionStatus.COMPLETED;
    try {
      last = visitStep(program.getStartStepId());
    } catch(StepException e) {
      LOGGER.error(e.getMessage(), e);
      status = FlowExecutionStatus.ERROR;
      last = visitException(e);
    }
    
    final List logs = new ArrayList<>(stepLogs.values());
    if(last != null && status == FlowExecutionStatus.ERROR) {
      logs.add(last);
    }
    Collections.sort(logs, (o1, o2) -> Integer.compare(o1.getId(), o2.getId()));
    
    return ImmutableFlowResult.builder()
        .logs(logs)
        .stepId(last.getStepId())
        .status(status)
        .accepts(accepted)
        .returns(last.getReturns())
        .shortHistory(shortHistory.toString())
        .build();
  }
  
  private FlowResultLog visitException(Exception e) {
    final List logs = new ArrayList<>(stepLogs.values());
    Collections.sort(logs, (o1, o2) -> Integer.compare(o1.getId(), o2.getId()));
    
    
    final var rootCause = ExceptionUtils.getRootCause(e);
    final var rootMsg = ExceptionUtils.getRootCauseStackTrace(rootCause);
    final var messages = new ArrayList();
    
    final var traceBuilder = new StringBuilder();
    for(final var trace : rootMsg) {
      if(trace.contains("resys")) {
        traceBuilder.append(trace);
      }
    }
    
    messages.add(ImmutableFlowResultErrorLog.builder().id("error").msg(e.getMessage() == null ? "" : e.getMessage()).build());
    messages.add(ImmutableFlowResultErrorLog.builder().id("trace").msg(traceBuilder.toString()).build());
    
    final FlowResultLog lastLog;
    if(logs.isEmpty()) {
      lastLog = ImmutableFlowResultLog.builder()
          .id(0)
          .stepId("start")
          .start(start)
          .end(LocalDateTime.now())
          .status(FlowExecutionStatus.ERROR)
          .addAllErrors(messages)
          .build();        
    } else {
      lastLog = ImmutableFlowResultLog.builder()
          .from(logs.get(logs.size() - 1))
          .addAllErrors(messages)
          .build();
    }
    return lastLog;
  }
  
  private FlowResultLog visitStepLog(FlowResultLog log) {
    visitShortHistory(log);
    this.stepLogs.put(log.getStepId(), log);
    return log;
  }

  
  private FlowResultLog visitStep(String stepId) {
    final var step = program.getSteps().get(stepId);    
    final var log = visitBody(step);
    
    switch (step.getPointer().getType()) {
    case THEN: return visitThenPointer(step);
    case SWITCH: return visitSwitchPointer(step);
    case END: return log;
    default: throw new ProgramException("Step pointer: '" + step.getPointer().getType() + "' not implemented!");
    }
  }
  
  private FlowResultLog visitBody(FlowProgramStep step) {
    final var start = LocalDateTime.now();
    if(step.getBody() == null) {
      return visitStepLog(
          ImmutableFlowResultLog.builder()
          .id(this.stepLogs.size() + 1)
          .stepId(step.getId())
          .start(start)
          .end(LocalDateTime.now())
          .status(FlowExecutionStatus.COMPLETED)
          .build());
    }
    
    final var inputs = visitInputMapping(step);
    
    switch (step.getBody().getRefType()) {
    case DT: {
      final var program = context.getDecision(step.getBody().getRef());
      try {
        final var result = DecisionProgramExecutor.run(program, ImmutableProgramContext.from(context).map(inputs).build());
        final var outputs = step.getBody().getCollection() ? 
            Map.of("", (Serializable) DecisionProgramExecutor.find(result)): 
            DecisionProgramExecutor.get(result);
          
        return visitStepLog(ImmutableFlowResultLog.builder()
            .id(this.stepLogs.size() + 1)
            .stepId(step.getId())
            .start(start)
            .end(LocalDateTime.now())
            .status(FlowExecutionStatus.COMPLETED)
            .accepts(inputs)
            .returns(toNonNull(outputs))
            .returnsValue((Serializable) outputs)
            .build());
      } catch(Exception e) {
        visitStepLog(ImmutableFlowResultLog.builder()
            .id(this.stepLogs.size() + 1)
            .stepId(step.getId())
            .start(start)
            .end(LocalDateTime.now())
            .status(FlowExecutionStatus.ERROR)
            .accepts(inputs)
            .build());
        throw new StepException(e.getMessage(), e);
      }
    }
    case SERVICE: {
      final var program = context.getService(step.getBody().getRef());
      final var log = ImmutableFlowExecutionLog.builder().putAllSteps(stepLogs).putAllAccepts(inputs).build();
      try { 
        final var result = ServiceProgramExecutor.run(program, ImmutableProgramContext.from(context).log(log).map(inputs).build());
        final var outputs = factory.toMap(result.getValue());
        return visitStepLog(ImmutableFlowResultLog.builder()
            .id(this.stepLogs.size() + 1)
            .stepId(step.getId())
            .start(start)
            .end(LocalDateTime.now())
            .status(FlowExecutionStatus.COMPLETED)
            .accepts(inputs)
            .returnsValue((Serializable) result.getValue())
            .returns(toNonNull(outputs))
            .build());
      } catch(Exception e) {
        visitStepLog(ImmutableFlowResultLog.builder()
            .id(this.stepLogs.size() + 1)
            .stepId(step.getId())
            .start(start)
            .end(LocalDateTime.now())
            .status(FlowExecutionStatus.ERROR)
            .accepts(inputs)
            .build());
        throw new StepException(e.getMessage(), e);
      }
    }
    default: 
      throw new ProgramException("Flow step: '" + step.getId() + "' ref: '" + step.getBody().getRefType() + "' is not supported !");
    }
  }
  
  private Map visitInputMapping(FlowProgramStep step) {
    final Map result = new HashMap<>();
    for(final var entry : step.getBody().getInputMapping().entrySet()) {
      String nameOnService = entry.getKey();
      
      try {
        // Flat mapping
        Serializable value;
        if(accepted.containsKey(entry.getValue())) {
          value = accepted.get(entry.getValue());
        } else {
          value = visitVariableOnPath(entry.getValue());
        }
        
        if(value != null) {
          result.put(nameOnService, value);
        }
        
      } catch(Exception e) {
        throw new ProgramException(
            "Failed to get parameter: '" + entry.getKey() + ":" + entry.getValue() + "' while mapping step: '" + step.getId() + "'" + System.lineSeparator() + 
            e.getMessage(), e);
      }
    }
    return result;
  }
  
  private FlowResultLog visitThenPointer(FlowProgramStep step) {
    final var stepId = ((FlowProgramStepThenPointer) step.getPointer()).getStepId();
    return visitStep(stepId);
  }
  
  private FlowResultLog visitSwitchPointer(FlowProgramStep step) {
    for(final var whenThen : ((FlowProgramStepWhenThenPointer) step.getPointer()).getConditions()) {
      boolean isMatch = (Boolean) whenThen.getExpression().run(expressionContext).getValue();
      if(isMatch) {
        return visitStep(whenThen.getStepId());
      }
    }
    throw new ProgramException("Flow switch: '" + step.getId() + "' does not match any expressions!");
  }
  
  private Map visitAcceptedDef(FlowProgram program, ProgramContext context) {
    Map result = new HashMap<>();
    List required = new ArrayList<>();
    for(final var dataType : program.getAcceptDefs()) {
      Serializable value = context.getValue(dataType);
      if(value != null) {
        result.put(dataType.getName(), value);
      }
      if(dataType.isRequired() && value == null) {
        required.add(dataType.getName());
      }
    }
    if(!required.isEmpty()) {
      throw new ProgramException("Flow can't have null inputs: " + String.join(", ", required) + "!");
    }
    
    return result;
  }

  @SuppressWarnings("unchecked")
  private Serializable visitVariableOnPath(String name) {
    String[] paths = name.split("\\.");
    if(paths.length == 0) {
      return null;
    }

    Map prev = null;
    StringBuilder fullName = new StringBuilder();
    int index = 0;
    for(String path : paths) {
      index++;
      final var isLast = index == paths.length;
      if(fullName.length() > 0) {
        fullName.append(".");
      }
      fullName.append(path);
      
      
      // first parameter
      if(prev == null) {
        // resolve based on accepted
        if(accepted.containsKey(path)) {
          Object target = accepted.get(path);
          if(Map.class.isAssignableFrom(target.getClass())) {
            prev = (Map) target;
          } else if(!isLast) {
            prev = (Map) factory.toMap(target);
          } else {
            return (Serializable) target;
          }
          continue;
        }
      
        // resolve from executed steps
        if(stepLogs.containsKey(path)) {
          FlowResultLog target = stepLogs.get(path);
          prev = target.getReturns();
          if(isLast) {
            return (Serializable) prev;
          }
          continue; 
        }
        
        // root context
        final ProgramContextNamedValue contextValue = context.getValueWithMeta(path);
        if(contextValue.getFound()) {
          Serializable result = contextValue.getValue();
          if(isLast) {
            return result;
          } else if(contextValue instanceof Map) {
            prev = (Map) result;
          }
        }
      }
      
      
      if(prev == null) {
        //throw new ProgramException("Can't find parameter with name: '" + name + "'!");
        return null;
      }
      

      if(prev.containsKey(path) && isLast) {
        return prev.get(path);
      } else if(prev.containsKey(path) && !isLast) {
        prev = (Map) prev.get(path);
      } else {
        //throw new ProgramException("Can't find parameter with name: '" + name + "' from: '" + fullName + "'!");
        return null;
      }
    }
    
    return null;
  }
  
  
  public void visitShortHistory(FlowResultLog log) {
    if(shortHistory.length() > 0) {
      shortHistory.append(" -> ");
    }
    
    if(stepLogs.containsKey(log.getStepId())) {
      shortHistory.append("(recursion) ");
      shortHistory.append(System.lineSeparator() + getIndent(log));
    }
    shortHistory.append(log.getStepId());
  }
  
  private String getIndent(FlowResultLog previous) {
    StringBuilder result = new StringBuilder();
    for(int index = 0; index <= previous.getId(); index++) {
      result.append("  ");
    }
    return result.toString();
  }
  
  private Map toNonNull(Map input) {
    Map result = new HashMap<>();
    input.entrySet().stream().filter(e -> e.getValue() != null && e.getKey() != null)
    .forEach(e -> result.put(e.getKey(), e.getValue()));
    
    return result;
  }
  
  
  private static class StepException extends RuntimeException {
    private static final long serialVersionUID = 2352180316876534777L;

    public StepException(String message, Throwable cause) {
      super(message, cause);
    }
    
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy