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

com.deliveredtechnologies.rulebook.model.GoldenRule Maven / Gradle / Ivy

There is a newer version: 0.12
Show newest version
package com.deliveredtechnologies.rulebook.model;

import com.deliveredtechnologies.rulebook.NameValueReferable;
import com.deliveredtechnologies.rulebook.NameValueReferableMap;
import com.deliveredtechnologies.rulebook.TypeConvertibleFactMap;
import com.deliveredtechnologies.rulebook.NameValueReferableTypeConvertibleMap;
import com.deliveredtechnologies.rulebook.RuleState;
import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.FactMap;
import com.deliveredtechnologies.rulebook.util.ArrayUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.function.Consumer;
import java.util.function.BiConsumer;
import java.util.function.Predicate;

import static com.deliveredtechnologies.rulebook.model.RuleChainActionType.CONTINUE_ON_FAILURE;
import static com.deliveredtechnologies.rulebook.model.RuleChainActionType.STOP_ON_FAILURE;
import static com.deliveredtechnologies.rulebook.model.RuleChainActionType.ERROR_ON_FAILURE;


/**
 * A standard implementation of {@link Rule}.
 * @param  the fact type
 * @param  the Result type
 */
public class GoldenRule implements Rule {

  private static Logger LOGGER = LoggerFactory.getLogger(GoldenRule.class);

  private NameValueReferableMap _facts = new FactMap();
  private Result _result;
  private Predicate> _condition;
  private List _actionChain = new ArrayList<>();
  private Map> _factNames = new HashMap<>();
  private RuleState _ruleState = RuleState.NEXT;
  private Class _factType;
  private RuleChainActionType _actionType;

  public GoldenRule(Class factType) {
    this(factType, CONTINUE_ON_FAILURE);
  }

  public GoldenRule(Class factType, RuleChainActionType actionType) {
    _factType = factType;
    _actionType = actionType;
  }

  @Override
  @SuppressWarnings("unchecked")
  public void addFacts(NameValueReferable... facts) {
    Arrays.stream(facts).forEach(fact -> _facts.put(fact.getName(), fact));
  }

  @Override
  @SuppressWarnings("unchecked")
  public void addFacts(NameValueReferableMap facts) {
    _facts.putAll(facts);
  }

  @Override
  public void setFacts(NameValueReferableMap facts) {
    _facts = facts;
  }

  @Override
  public void setCondition(Predicate> condition) {
    _condition = condition;
  }

  @Override
  public void setRuleState(RuleState ruleState) {
    _ruleState = ruleState;
  }

  @Override
  public void addAction(Consumer> action) {
    if (!_actionChain.contains(action)) {
      _actionChain.add(action);
    }
  }

  @Override
  public void addAction(BiConsumer, Result> action) {
    if (!_actionChain.contains(action)) {
      _actionChain.add(action);
    }
  }

  @Override
  public void addFactNameFilter(String... factNames) {
    List factNameList = Stream.of(factNames).collect(Collectors.toList());
    if (_factNames.containsKey((getActions()).size())) {
      List existingFactNames = _factNames.get((getActions()).size());
      existingFactNames.addAll(factNameList);
    } else {
      _factNames.put((getActions()).size(), factNameList);
    }
  }

  @Override
  public NameValueReferableMap getFacts() {
    return _facts;
  }

  @Override
  public Predicate> getCondition() {
    return _condition;
  }

  @Override
  public RuleState getRuleState() {
    return _ruleState;
  }

  @Override
  public List getActions() {
    return _actionChain;
  }

  @Override
  @SuppressWarnings("unchecked")
  public boolean invoke(NameValueReferableMap facts) {
    try {
      //only use facts of the specified type
      NameValueReferableMap typeFilteredFacts =
              new FactMap((Map>) facts.keySet().stream()
              .filter((Object key) -> {
                NameValueReferable fact = ((NameValueReferable)facts.get(key));
                return fact.getValue() == null || _factType.isAssignableFrom(fact.getValue().getClass());
              })
              .collect(Collectors.toMap(
                  key -> key,
                  key -> ((NameValueReferable) facts.get(key)))));

      //invoke then() action(s) if when() is true or if when() was never specified
      if (getCondition() == null || getCondition().test(new TypeConvertibleFactMap(typeFilteredFacts))) {

        //iterate through the then() actions specified
        List actionList = getActions();
        for (int i = 0; i < (getActions()).size(); i++) {
          Object action = actionList.get(i);
          List factNames = _factNames.get(i);

          //if using() fact names were specified for the specific then(), use only those facts specified
          NameValueReferableMap usingFacts;
          if (factNames != null) {
            usingFacts = new FactMap(factNames.stream()
                    .filter(typeFilteredFacts::containsKey)
                    .collect(Collectors.toMap(name -> name, name -> (NameValueReferable)facts.get(name))));
          } else {
            usingFacts = typeFilteredFacts;
          }

          //setup a dummy Result just in case a Result is used
          Result result = new Result();

          //invoke the action
          Stream.of(action.getClass().getMethods())
                  .filter(method -> method.getName().equals("accept"))
                  .findFirst()
                    .ifPresent(method -> {
                      try {
                        method.setAccessible(true);
                        method.invoke(action,
                            ArrayUtils.combine(
                                    new Object[]{new TypeConvertibleFactMap<>(usingFacts)},
                                    new Object[]{getResult().orElseGet(() -> result)},
                                    method.getParameterCount()));
                        if (result.getValue() != null) {
                          _result = result;
                        }
                      } catch (IllegalAccessException | InvocationTargetException err) {
                        LOGGER.error("Error invoking action on " + action.getClass(), err);
                        if (_actionType.equals(ERROR_ON_FAILURE)) {
                          throw err.getCause() == null ? new RuleException(err) :
                              err.getCause() instanceof RuleException ? (RuleException)err.getCause() :
                                  new RuleException(err.getCause());
                        }
                      }
                    });
          facts.putAll(usingFacts);
        }

        // the actions were executed, so the rule chain continues
        // STOP_ON_FAILURE ONLY stops the rule chain if there is a failure
        if (_actionType.equals(STOP_ON_FAILURE)) {
          this._ruleState = RuleState.NEXT;
        }
        return true; //signifies that the actions were executed
      }
    } catch (Exception ex) {
      //catch errors from the 'when' condition; if it fails, just continue along unless ERROR_ON_FAILURE is set
      LOGGER.error("Error occurred when trying to evaluate rule!", ex);
      if (_actionType.equals(ERROR_ON_FAILURE)) {
        throw ex instanceof RuleException ? (RuleException)ex : new RuleException(ex);
      }
    }

    return _actionType.equals(STOP_ON_FAILURE);
  }

  @Override
  public void setResult(Result result) {
    _result = result;
  }

  @Override
  public Optional> getResult() {
    return Optional.ofNullable(_result);
  }
}