package com.deliveredtechnologies.rulebook;

import java.util.Map;
import java.util.HashMap;
import java.util.Optional;
import java.util.Set;
import java.util.Collection;

/**
 * A FactMap decorates {@link Map}; it stores facts by their name and provides convenience methods for
 * accessing {@link Fact} objects.
 */
public class FactMap<T> implements NameValueReferableMap<T> {

  private Map<String, NameValueReferable<T>> _facts;

  public FactMap(Map<String, NameValueReferable<T>> facts) {
    _facts = facts;
  }

  /**
   * Constructor allows a FactMap to be created by specifying NameValueReferable facts.
   * @param facts an array of NameValueReferable facts
   */
  @SafeVarargs
  public FactMap(NameValueReferable<T>... facts) {
    this();

    for (NameValueReferable<T> fact : facts) {
      this.put(fact);
    }
  }

  public FactMap() {
    _facts = new HashMap<>();
  }

  /**
   * The method getOne() gets the value of the single Fact in the FactMap.
   * @return the value of a fact stored if only one fact is stored, otherwise null
   */
  @Override
  public T getOne() {
    if (_facts.size() == 1) {
      return _facts.values().iterator().next().getValue();
    }
    return null;
  }

  /**
   * The method getValue() returns the value of the Fact associated with the name passed in.
   * @param name  the name of the Fact
   * @return      the value of the Fact
   */
  @Override
  public T getValue(String name) {
    return Optional.ofNullable(_facts.get(name)).map(NameValueReferable::getValue).orElse(null);
  }

  /**
   * Method getStrVal() gets the String value of a Fact in the FactMap.
   * @param name  the name of the Fact in the FactMap
   * @return      the String value of the Fact specified
   */
  @Deprecated
  public String getStrVal(String name) {
    if (getValue(name) instanceof String) {
      return (String)getValue(name);
    }
    return String.valueOf(getValue(name));
  }

  /**
   * Method getIntVal() gets the Integer value of a Fact in the FactMap.
   * @param name  the name of the Fact in the FactMap
   * @return      the Integer value of the Fact specified
   */
  @Deprecated
  public Integer getIntVal(String name) {
    Object value = getValue(name);
    if (value != null) {
      if (Integer.class == value.getClass()) {
        return (Integer)value;
      }
      if (value.getClass() == String.class) {
        return Integer.valueOf((String) value);
      }
    }
    return null;
  }

  /**
   * Method getDblVal() gets the Double value of a Fact in the FactMap.
   * @param name  the name of the Fact in the FactMap
   * @return      the Double value of the Fact specified
   */
  @Deprecated
  public Double getDblVal(String name) {
    Object value = getValue(name);
    if (value != null) {
      if (Float.class == value.getClass()) {
        return Double.valueOf((Float) value);
      }
      if (Double.class == value.getClass()) {
        return (Double)value;
      }
      if (Integer.class == value.getClass()) {
        return Double.valueOf((Integer) value);
      }
      if (Long.class == value.getClass()) {
        return Double.valueOf((Long) value);
      }
      if (String.class == value.getClass()) {
        return Double.parseDouble((String)value);
      }
    }
    return null;
  }

  /**
   * The method setValue sets the value of the Fact by its name.

   * If no Fact exists with the associated name, a new fact is created with the specified name and value.

   * @param name  the name of the Fact
   * @param value the value object of the Fact
   */
  public void setValue(String name, T value) {
    NameValueReferable<T> fact = _facts.get(name);
    if (fact == null) {
      fact = new Fact<>(name, value);
      _facts.put(name, fact);
      return;
    }
    fact.setValue(value);
  }

  /**
   * This put() method is a convenience method for adding a Fact to a FactMap.

   * It uses the name of the Fact as the key and the Fact as the value.
   * @param fact  the Fact to be added to the FactMap
   * @return      the Fact that was just added
   */
  @Override
  public Fact<T> put(NameValueReferable<T> fact) {
    return put(fact.getName(), fact);
  }

  @Override
  public Fact<T> put(String key, NameValueReferable<T> fact) {
    Optional<NameValueReferable<T>> prev = Optional.ofNullable(_facts.put(key, fact));
    return prev.map(obj -> obj instanceof Fact ? (Fact<T>)obj : new Fact<>((NameValueReferable<T>)obj)).orElse(null);
  }

  @Override
  public int size() {
    return _facts.size();
  }

  @Override
  public boolean isEmpty() {
    return _facts.isEmpty();
  }

  @Override
  public boolean containsKey(Object key) {
    return _facts.containsKey(key);
  }

  @Override
  public boolean containsValue(Object value) {
    return _facts.containsValue(value);
  }

  @Override
  public Fact<T> get(Object key) {
    NameValueReferable<T> obj = _facts.get(key);
    if (obj == null) {
      return null;
    }
    if (obj instanceof Fact) {
      return (Fact<T>)obj;
    }
    return new Fact<>(obj);
  }

  @Override
  public Fact<T> remove(Object key) {
    NameValueReferable<T> obj = _facts.remove(key);
    if (obj == null) {
      return null;
    }
    if (obj instanceof Fact) {
      return (Fact<T>)obj;
    }
    return new Fact<>(obj);
  }

  @Override
  public void putAll(Map<? extends String, ? extends NameValueReferable<T>> map) {
    _facts.putAll(map);
  }

  @Override
  public void clear() {
    _facts.clear();
  }

  @Override
  public Set<String> keySet() {
    return _facts.keySet();
  }

  @Override
  public Collection<NameValueReferable<T>> values() {
    return _facts.values();
  }

  @Override
  public Set<Entry<String, NameValueReferable<T>>> entrySet() {
    return _facts.entrySet();
  }

  /**
   * The toString() method gets the FactMap converted a string.

   * If there is only one Fact, the String value is the String value of the Fact.

   * Otherwise, the String value is the value of the parent {@link HashMap}.
   * @return  the String value of the FactMap
   */
  @Override
  public String toString() {
    return _facts.size() == 1 ? this.getOne().toString() : _facts.toString();
  }
}


package com.deliveredtechnologies.rulebook.lang;

import com.deliveredtechnologies.rulebook.NameValueReferableMap;
import com.deliveredtechnologies.rulebook.NameValueReferable;
import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.NameValueReferableTypeConvertibleMap;
import com.deliveredtechnologies.rulebook.Fact;
import com.deliveredtechnologies.rulebook.model.Rule;

import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * Builds the portion of a Rule that is available in the language on 'given.'
 */
public class GivenRuleBuilder<T, U> {
  Rule<T, U> _rule;

  GivenRuleBuilder(Rule<T, U> rule, NameValueReferableMap facts) {
    _rule = rule;
    given(facts);
  }

  @SafeVarargs
  GivenRuleBuilder(Rule<T, U> rule, NameValueReferable... facts) {
    _rule = rule;
    given(facts);
  }

  /**
   * Adds a fact to the Rule.
   * @param name  the name of the fact
   * @param value the value of the fact
   * @return      a GivenRuleBuilder
   */
  GivenRuleBuilder<T, U> given(String name, T value) {
    return given(new Fact<T>(name, value));
  }

  /**
   * Adds one or more facts into the Rule using a {@link NameValueReferableMap}.
   * @param   facts the facts to be added to the Rule
   * @return  the current builder object
   */
  public final GivenRuleBuilder<T, U> given(NameValueReferableMap facts) {
    _rule.setFacts(facts);
    return this;
  }

  /**
   * Adds one or more facts into the Rule.
   * @param   facts one or more facts
   * @return  the current builder object
   */
  @SafeVarargs
  public final GivenRuleBuilder<T, U> given(NameValueReferable... facts) {
    _rule.addFacts(facts);
    return this;
  }

  /**
   * Adds the condition for the Rule.
   * @param condition the condition specified for the Rule
   * @return          a builder that allows for the Rule to be built following the condition
   */
  public WhenRuleBuilder<T, U> when(Predicate<NameValueReferableTypeConvertibleMap<T>> condition) {
    return new WhenRuleBuilder<T, U>(_rule, condition);
  }

  /**
   * Adds a using constraint in the Rule that restricts the facts supplied to the subsequent 'then' action.
   * @param factNames the fact names to be supplied to the subsequent 'then' action
   * @return          a builder the allows for the Rule to be built following the 'using' statement
   */
  public UsingRuleBuilder<T, U> using(String... factNames) {
    return new UsingRuleBuilder<T, U>(_rule, factNames);
  }

  /**
   * Adds a then action into the Rule.
   * @param action  an action that the rule will execute based on the condition
   * @return        a builder that allows for the Rule to be built following the 'then' statement
   */
  public ThenRuleBuilder<T, U> then(Consumer<NameValueReferableTypeConvertibleMap<T>> action) {
    return new ThenRuleBuilder<T, U>(_rule, action);
  }

  /**
   * Addds a then action into the Rule.
   * @param action  an action that the rule will execute based on the condition; accepts facts and the result
   * @return        a builder that allows for the Rule to be built following the 'then' statement
   */
  public ThenRuleBuilder<T, U> then(BiConsumer<NameValueReferableTypeConvertibleMap<T>, Result<U>> action) {
    return new ThenRuleBuilder<T, U>(_rule, action);
  }
}


package com.deliveredtechnologies.rulebook.lang;

import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.Fact;
import com.deliveredtechnologies.rulebook.NameValueReferable;
import com.deliveredtechnologies.rulebook.NameValueReferableMap;
import com.deliveredtechnologies.rulebook.NameValueReferableTypeConvertibleMap;
import com.deliveredtechnologies.rulebook.model.AuditableRule;
import com.deliveredtechnologies.rulebook.model.GoldenRule;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.RuleChainActionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
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;

/**
 * The initial builder used to build a Rule.
 * @param <T> the type of facts used in the Rule
 * @param <U> the Result type used in the Rule
 */
public class RuleBuilder<T, U> implements TerminatingRuleBuilder<T, U> {

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

  private Class<? extends Rule> _ruleClass;
  private Class<T> _factType;
  private Class<U> _resultType;
  private RuleChainActionType _actionType = CONTINUE_ON_FAILURE;
  private Optional<String> _name = Optional.empty();

  /**
   * Returns a new RuleBuilder for the specified Rule class.
   * @param ruleClass   the class of Rule to build
   * @param actionType  if STOP_ON_FAILURE, stops the rule chain if RuleState is BREAK only if the rule fails
   *                    (for supported rule classes); if ERROR_ON_FAILURE, allows exceptions to be thrown from rule;
   *                    default is CONTINUE_ON_FAILURE
   * @return            a new RuleBuilder
   */
  public static RuleBuilder<Object, Object> create(Class<? extends Rule> ruleClass,
                                                   RuleChainActionType actionType) {
    return new RuleBuilder<>(ruleClass, actionType);
  }

  /**
   * Returns a new RuleBuilder for the specified Rule class.
   * @param ruleClass the class of Rule to build
   * @return          a new RuleBuilder
   */
  public static RuleBuilder<Object, Object> create(Class<? extends Rule> ruleClass) {
    return new RuleBuilder<>(ruleClass);
  }

  /**
   * Returns a new RuleBuilder for the default Rule type.
   * @return  a new RuleBuilder
   */
  public static RuleBuilder<Object, Object> create() {
    RuleBuilder<Object, Object> rule = new RuleBuilder<>(GoldenRule.class);
    rule._factType = Object.class;
    return rule;
  }

  private RuleBuilder(Class<? extends Rule> ruleClass) {
    _ruleClass = ruleClass;
  }

  private RuleBuilder(Class<? extends Rule> ruleClass, RuleChainActionType actionType) {
    this(ruleClass);
    _actionType = actionType;
  }

  public RuleBuilder<T, U> withName(String name) {
    _name = Optional.of(name);
    return this;
  }

  /**
   * Specifies the fact type for the Rule being built.
   * @param factType  the type of facts to be used in the Rule
   * @param <S>       the type of facts
   * @return          a builder using the new fact type for building a Rule
   */
  public <S> RuleBuilder<S, U> withFactType(Class<S> factType) {
    RuleBuilder<S, U> builder = new RuleBuilder<>(_ruleClass);
    builder._factType = factType;
    builder._resultType = _resultType;
    builder._actionType = _actionType;
    return builder;
  }

  /**
   * Specifies the Result type for the Rule being built.
   * @param resultType  the type of the Result to be used in the Rule
   * @param <S>         the type of Result
   * @return            a builder using a new result type for building a Rule
   */
  public <S> RuleBuilder<T, S> withResultType(Class<S> resultType) {
    RuleBuilder<T, S> builder = new RuleBuilder<>(_ruleClass);
    builder._factType = _factType;
    builder._resultType = resultType;
    builder._actionType = _actionType;
    builder._name = _name;
    return builder;
  }

  /**
   * Adds a fact to the Rule using a name value pair to specify a new fact.
   * @param name    the name of the fact
   * @param value   the value of the fact
   * @return        a builder for building rules after a 'given' statement
   */
  @SuppressWarnings("unchecked")
  public GivenRuleBuilder<T, U> given(String name, T value) {
    Rule<T, U> rule = _name.map(ruleName -> (Rule<T, U>)new AuditableRule<T, U>(newRule(), ruleName)).orElse(newRule());
    if (rule == null) {
      throw new IllegalStateException("No Rule is instantiated; An invalid Rule class may have been provided");
    }
    return new GivenRuleBuilder<T, U>(rule, new Fact<T>(name, value));
  }

  /**
   * Adds one or more facts to the Rule.
   * @param facts the facts to be added to the Rule
   * @return  a builder for building rules after a 'given' statement
   */
  @SuppressWarnings("unchecked")
  @SafeVarargs
  public final GivenRuleBuilder<T, U> given(NameValueReferable... facts) {
    Rule<T, U> rule = _name.map(name -> (Rule<T, U>)new AuditableRule<T, U>(newRule(), name)).orElse(newRule());
    if (rule == null) {
      throw new IllegalStateException("No Rule is instantiated; An invalid Rule class may have been provided");
    }
    return new GivenRuleBuilder<T, U>(rule, facts);
  }

  /**
   * Adds facts to a Rule.
   * @param   facts the facts to be added to the Rule
   * @return  a builder for building rules after a 'given' statement
   */
  @SuppressWarnings("unchecked")
  public final GivenRuleBuilder<T, U> given(NameValueReferableMap facts) {
    Rule<T, U> rule = _name.map(name -> (Rule<T, U>)new AuditableRule<T, U>(newRule(), name)).orElse(newRule());
    if (rule == null) {
      throw new IllegalStateException("No Rule is instantiated; An invalid Rule class may have been provided");
    }
    return new GivenRuleBuilder<T, U>(rule, facts);
  }

  /**
   * Specifies the condition for the Rule.
   * @param condition the condition for the Rule
   * @return          a builder for building rules after a 'when' statement
   */
  @SuppressWarnings("unchecked")
  public WhenRuleBuilder<T, U> when(Predicate<NameValueReferableTypeConvertibleMap<T>> condition) {
    Rule<T, U> rule = _name.map(name -> (Rule<T, U>)new AuditableRule<T, U>(newRule(), name)).orElse(newRule());
    if (rule == null) {
      throw new IllegalStateException("No Rule is instantiated; An invalid Rule class may have been provided");
    }
    return new WhenRuleBuilder<T, U>(rule, condition);
  }

  /**
   * Adds a using constraint in the Rule that restricts the facts supplied to the subsequent 'then' action.
   * @param factNames the fact names to be supplied to the subsequent 'then' action
   * @return          a builder the allows for the Rule to be built following the 'using' statement
   */
  @SuppressWarnings("unchecked")
  public UsingRuleBuilder<T, U> using(String... factNames) {
    Rule<T, U> rule = _name.map(name -> (Rule<T, U>)new AuditableRule<T, U>(newRule(), name)).orElse(newRule());
    if (rule == null) {
      throw new IllegalStateException("No Rule is instantiated; An invalid Rule class may have been provided");
    }
    return new UsingRuleBuilder<T, U>(rule, factNames);
  }

  /**
   * Adds an action as a Consumer to the Rule.
   * @param action  a Consumer action to be added to the Rule that accepts the facts specified for the Rule
   * @return        a builder for building rules after a 'then' action is specified
   */
  @SuppressWarnings("unchecked")
  public ThenRuleBuilder<T, U> then(Consumer<NameValueReferableTypeConvertibleMap<T>> action) {
    Rule<T, U> rule = _name.map(name -> (Rule<T, U>)new AuditableRule<T, U>(newRule(), name)).orElse(newRule());
    if (rule == null) {
      throw new IllegalStateException("No Rule is instantiated; An invalid Rule class may have been provided");
    }
    return new ThenRuleBuilder<T, U>(rule, action);
  }

  /**
   * Adds an action as a Consumer to the Rule.
   * @param action  a Consumer action to be added to the Rule that accepts the facts and the Result specified
   *                for the Rule
   * @return        a builder for building rules after a 'then' action is specified
   */
  @SuppressWarnings("unchecked")
  public ThenRuleBuilder<T, U> then(BiConsumer<NameValueReferableTypeConvertibleMap<T>, Result<U>> action) {
    Rule<T, U> rule = _name.map(name -> (Rule<T, U>)new AuditableRule<T, U>(newRule(), name)).orElse(newRule());
    if (rule == null) {
      throw new IllegalStateException("No Rule is instantiated; An invalid Rule class may have been provided");
    }
    return new ThenRuleBuilder<T, U>(rule, action);
  }

  /**
   * Builds the Rule.
   * @return  a Rule
   */
  @Override
  public Rule<T, U> build() {
    return newRule();
  }

  private Rule<T, U> newRule() {
    if (_actionType.equals(STOP_ON_FAILURE)) {
      try {
        Constructor<?> constructor = _ruleClass.getConstructor(Class.class, RuleChainActionType.class);
        return (Rule<T, U>)constructor.newInstance(new Object[]{_factType, _actionType});
      } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException ex) {
        LOGGER.error("Unable to create an instance of the specified Rule class '"
            + _ruleClass
            + "' with the specified fact type and 'stopOnRuleFailire' boolean parameter");
        return null;
      }
    }
    try {
      Constructor<?> constructor = _ruleClass.getConstructor(Class.class, Class.class);
      return (Rule<T, U>)constructor.newInstance(new Object[]{_factType, _resultType});
    } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException ex) {
      LOGGER.debug("Unable to create an instance of the specified Rule class '"
          + _ruleClass
          + "' with fact and result types specified");
    }
    try {
      Constructor<?> constructor = _ruleClass.getConstructor(Class.class);
      return (Rule<T, U>)constructor.newInstance(new Object[] {_factType});
    } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException ex) {
      LOGGER.debug("Attempt to use a single argument constructor for specifying the Fact type failed", ex);
    }
    try {
      Constructor<?> constructor = _ruleClass.getConstructor(new Class[] {});
      return (Rule<T, U>)constructor.newInstance(new Object[]{});
    } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException ex) {
      LOGGER.debug("Attempt to use the default constructor failed for " + _ruleClass, ex);
      LOGGER.error("Unable to create a Rule of type '" + _ruleClass.getName() + "'");
    }

    return null;
  }
}


package com.deliveredtechnologies.rulebook.model.runner;

import com.deliveredtechnologies.rulebook.NameValueReferable;
import com.deliveredtechnologies.rulebook.NameValueReferableMap;
import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.RuleState;
import com.deliveredtechnologies.rulebook.annotation.Given;
import com.deliveredtechnologies.rulebook.annotation.Then;
import com.deliveredtechnologies.rulebook.annotation.When;
import com.deliveredtechnologies.rulebook.model.GoldenRule;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.RuleChainActionType;
import com.deliveredtechnologies.rulebook.model.RuleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InvalidClassException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Arrays;
import java.util.Optional;
import java.util.Collection;
import java.util.Set;
import java.util.Map;
import java.util.ArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.deliveredtechnologies.rulebook.util.AnnotationUtils.getAnnotatedField;
import static com.deliveredtechnologies.rulebook.util.AnnotationUtils.getAnnotatedMethods;
import static com.deliveredtechnologies.rulebook.util.AnnotationUtils.getAnnotatedFields;
import static com.deliveredtechnologies.rulebook.util.AnnotationUtils.getAnnotation;
/**
 * Adapts a {@link com.deliveredtechnologies.rulebook.annotation.Rule} annotated POJO to a
 * {@link Rule}.
 */
public class RuleAdapter implements Rule {

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

  private Rule _rule;
  private Object _pojoRule;
  private RuleChainActionType _actionType;

  /**
   * Adapts a POJO to a Rule given a POJO and a Rule.
   * @param pojoRule                the @Rule annotated POJO
   * @param rule                    a Rule object
   * @throws InvalidClassException  if the @Rule annotation is missing from the POJO
   */
  public RuleAdapter(Object pojoRule, Rule rule) throws InvalidClassException {
    com.deliveredtechnologies.rulebook.annotation.Rule ruleAnnotation =
        getAnnotation(com.deliveredtechnologies.rulebook.annotation.Rule.class, pojoRule.getClass());

    if (ruleAnnotation == null) {
      throw new InvalidClassException(pojoRule.getClass() + " is not a Rule; missing @Rule annotation");
    }

    _actionType = ruleAnnotation.ruleChainAction();
    _rule = rule == null ? new GoldenRule(Object.class, _actionType) : rule;
    _pojoRule = pojoRule;
  }

  /**
   * Adapts a POJO to a the default Rule type (i.e. {@link GoldenRule}).
   * @param pojoRule                the @Rule annotated POJO
   * @throws InvalidClassException  if the @Rule annotation is missing from the POJO
   */
  @SuppressWarnings("unchecked")
  public RuleAdapter(Object pojoRule) throws InvalidClassException {
    this(pojoRule, null);
  }

  @Override
  public void addFacts(NameValueReferable... facts) {
    _rule.addFacts(facts);
    mapFactsToProperties(_rule.getFacts());
  }

  @Override
  public void addFacts(NameValueReferableMap facts) {
    _rule.addFacts(facts);
    mapFactsToProperties(_rule.getFacts());
  }

  @Override
  public void setFacts(NameValueReferableMap facts) {
    _rule.setFacts(facts);
    mapFactsToProperties(_rule.getFacts());
  }

  @Override
  @SuppressWarnings("unchecked")
  public void setCondition(Predicate condition) throws IllegalStateException {
    _rule.setCondition(condition);
  }

  @Override
  public void setRuleState(RuleState ruleState) {
    _rule.setRuleState(ruleState);
  }

  @Override
  @SuppressWarnings("unchecked")
  public void addAction(Consumer action) {
    _rule.addAction(action);
  }

  @Override
  @SuppressWarnings("unchecked")
  public void addAction(BiConsumer action) {
    _rule.addAction(action);
  }

  @Override
  public void addFactNameFilter(String... factNames) {
    throw new UnsupportedOperationException();
  }

  @Override
  public NameValueReferableMap getFacts() {
    return _rule.getFacts();
  }

  @Override
  @SuppressWarnings("unchecked")
  public Predicate<NameValueReferableMap> getCondition() {
    //Use what was set by then() first, if it's there
    if (_rule.getCondition() != null) {
      return _rule.getCondition();
    }

    //If nothing was explicitly set, then convert the method in the class
    _rule.setCondition(Arrays.stream(_pojoRule.getClass().getMethods())
            .filter(method -> method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class)
            .filter(method -> Arrays.stream(method.getDeclaredAnnotations()).anyMatch(When.class::isInstance))
            .findFirst()
            .<Predicate>map(method -> object -> {
              try {
                return (Boolean) method.invoke(_pojoRule);
              } catch (InvocationTargetException | IllegalAccessException ex) {
                if (_actionType == RuleChainActionType.ERROR_ON_FAILURE) {
                  throw new RuleException(ex.getCause() == null ? ex : ex.getCause());
                }
                LOGGER.error(
                    "Unable to validate condition due to an exception. It will be evaluated as false", ex);
                return false;
              }
            })
            //If the condition still can't be determined, then just hand back one that returns true
            .orElse(o -> true));
    return _rule.getCondition();
  }

  @Override
  public RuleState getRuleState() {
    return _rule.getRuleState();
  }

  @Override
  @SuppressWarnings("unchecked")
  public List<Object> getActions() {
    if (_rule.getActions().size() < 1) {
      List<Object> actionList = new ArrayList<>();
      for (Method actionMethod : getAnnotatedMethods(Then.class, _pojoRule.getClass())) {
        actionMethod.setAccessible(true);
        Object then = getThenMethodAsBiConsumer(actionMethod).map(Object.class::cast)
            .orElse(getThenMethodAsConsumer(actionMethod).orElse(factMap -> { }));
        actionList.add(then);
      }
      _rule.getActions().addAll(actionList);
    }
    return _rule.getActions();
  }

  @Override
  @SuppressWarnings("unchecked")
  public boolean invoke(NameValueReferableMap facts) {
    mapFactsToProperties(facts);
    // getActions and getCondition are called here so that they could be overridden prior to calling invoke()
    getActions();
    getCondition();
    return _rule.invoke(facts);
  }

  @Override
  @SuppressWarnings("unchecked")
  public void setResult(Result result) {
    _rule.setResult(result);

    getAnnotatedField(com.deliveredtechnologies.rulebook.annotation.Result.class, _pojoRule.getClass())
          .ifPresent(field -> {
            field.setAccessible(true);
            try {
              if (result.getValue() != null
                  && (result.getValue().getClass() != Object.class
                  || field.getType().isAssignableFrom(result.getValue().getClass()))) {
                field.set(_pojoRule, result.getValue());
              }
            } catch (Exception ex) {
              LOGGER.error("Unable to set @Result field in " + _pojoRule.getClass(), ex);
            }
          });
  }

  @Override
  public Optional<Result> getResult() {
    return _rule.getResult();
  }

  /**
   * Convert the Facts to properties with the @Given annotation in the class.
   * If any matched properties are non-Facts, then the value of the associated Facts are mapped to those
   * properties. If any matched properties are Facts, then the Fact object are mapped to those properties.
   */
  @SuppressWarnings("unchecked")
  private void mapFactsToProperties(NameValueReferableMap facts) {
    for (Field field : getAnnotatedFields(Given.class, _pojoRule.getClass())) {
      Given given = field.getAnnotation(Given.class);
      try {
        field.setAccessible(true);
        if (NameValueReferable.class.isAssignableFrom(field.getType())) {
          field.set(_pojoRule, facts.get(given.value()));
        } else {
          Object value = facts.getValue(given.value());
          if (value != null) {
            //set the field to the Fact that has the name of the @Given value
            field.set(_pojoRule, value);
          } else if (NameValueReferableMap.class.isAssignableFrom(field.getType())) {
            //if the field is a FactMap then give it the FactMap
            field.set(_pojoRule, facts);
          } else if (Collection.class.isAssignableFrom(field.getType())) {
            //set a Collection of Fact object values
            Stream stream = facts.values().stream()
                    .filter(fact -> { //filter on only facts that contain objects matching the generic type
                      ParameterizedType paramType = (ParameterizedType)field.getGenericType();
                      Class<?> genericType = (Class<?>)paramType.getActualTypeArguments()[0];
                      return genericType.equals(((NameValueReferable) fact).getValue().getClass());
                    })
                    .map(fact -> {
                      ParameterizedType paramType = (ParameterizedType)field.getGenericType();
                      Class<?> genericType = (Class<?>)paramType.getActualTypeArguments()[0];
                      return genericType.cast(((NameValueReferable)fact).getValue());
                    });
            if (List.class == field.getType()) {
              //map List of Fact values to field
              field.set(_pojoRule, stream.collect(Collectors.toList()));
            } else if (Set.class == field.getType()) {
              //map Set of Fact values to field
              field.set(_pojoRule, stream.collect(Collectors.toSet()));
            }
          } else if (Map.class == field.getType()) {
            //map Map of Fact values to field
            Map map = (Map)facts.keySet().stream()
                    .filter(key -> {
                      ParameterizedType paramType = (ParameterizedType)field.getGenericType();
                      Class<?> genericType = (Class<?>)paramType.getActualTypeArguments()[1];
                      return genericType.equals(facts.getValue((String)key).getClass());
                    })
                    .collect(Collectors.toMap(key -> key, key -> facts.getValue((String)key)));
            field.set(_pojoRule, map);
          }
        }
      } catch (Exception ex) {
        LOGGER.error("Unable to update field '" + field.getName() + "' in rule object '"
                + _pojoRule.getClass() + "'", ex);
      }
    }
  }

  @SuppressWarnings("unchecked")
  private Optional<BiConsumer> getThenMethodAsBiConsumer(Method method) {
    return getAnnotatedField(com.deliveredtechnologies.rulebook.annotation.Result.class, _pojoRule.getClass())
            .map(resultField -> (BiConsumer) (facts, result) -> {
              try {
                Object retVal = method.invoke(_pojoRule);
                if (method.getReturnType() == RuleState.class && retVal == RuleState.BREAK) {
                  _rule.setRuleState(RuleState.BREAK);
                }
                resultField.setAccessible(true);
                Object resultVal = resultField.get(_pojoRule);
                ((com.deliveredtechnologies.rulebook.Result) result).setValue(resultVal);
              } catch (IllegalAccessException | InvocationTargetException ex) {
                if (_actionType == RuleChainActionType.ERROR_ON_FAILURE) {
                  throw new RuleException(ex.getCause() == null ? ex : ex.getCause());
                }
                LOGGER.error("Unable to invoke "
                    + _pojoRule.getClass().getName()
                    + " when converting then to BiConsumer", ex);
              }
            });
  }

  private Optional<Consumer> getThenMethodAsConsumer(Method method) {
    if (!getAnnotatedField(com.deliveredtechnologies.rulebook.annotation.Result.class,
            _pojoRule.getClass()).isPresent()) {
      return Optional.of((Consumer) obj -> {
        try {
          Object retVal = method.invoke(_pojoRule);
          if (method.getReturnType() == RuleState.class && retVal == RuleState.BREAK) {
            _rule.setRuleState(RuleState.BREAK);
          }
        } catch (IllegalAccessException | InvocationTargetException ex) {
          if (_actionType == RuleChainActionType.ERROR_ON_FAILURE) {
            throw new RuleException(ex.getCause() == null ? ex : ex.getCause());
          }
          LOGGER.error("Unable to invoke "
                  + _pojoRule.getClass().getName()
                  + " when converting then to Consumer", ex);
        }
      });
    }
    return Optional.empty();
  }
}


package com.deliveredtechnologies.rulebook.model.runner;

import com.deliveredtechnologies.rulebook.NameValueReferableMap;
import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.model.RuleBook;
import com.deliveredtechnologies.rulebook.model.Auditor;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.AuditableRule;
import com.deliveredtechnologies.rulebook.model.Auditable;

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

import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.util.List;
import java.util.Optional;

import static com.deliveredtechnologies.rulebook.util.AnnotationUtils.getAnnotatedField;
import static com.deliveredtechnologies.rulebook.util.AnnotationUtils.getAnnotation;

/**
 * Declares an abstract class for creating RuleBookRunner derived classes using the template method pattern.
 */
public abstract class AbstractRuleBookRunner extends Auditor implements RuleBook {
  private static Logger LOGGER = LoggerFactory.getLogger(RuleBookRunner.class);

  private Class<? extends RuleBook> _prototypeClass;

  @SuppressWarnings("unchecked")
  private Result _result = new Result(null);

  /**
   * Creates a new RuleBookRunner using the RuleBook class.
   * @param prototypeClass a RuleBook class that determines how the RuleBook is run
   */
  public AbstractRuleBookRunner(Class<? extends RuleBook> prototypeClass) {
    _prototypeClass = prototypeClass;
  }

  @Override
  public void addRule(Rule rule) {
    throw new UnsupportedOperationException("Rules are only added to a RuleBookRunner on run()!");
  }

  @Override
  @SuppressWarnings("unchecked")
  public void run(NameValueReferableMap facts) {
    getResult().ifPresent(Result::reset);
    try {
      RuleBook ruleBook = _prototypeClass.newInstance();
      List<Class<?>> classes = getPojoRules();
      for (Class<?> rule : classes) {
        getAnnotatedField(com.deliveredtechnologies.rulebook.annotation.Result.class, rule).ifPresent(field ->
            ruleBook.setDefaultResult(_result.getValue())
        );
        String name = getAnnotation(com.deliveredtechnologies.rulebook.annotation.Rule.class, rule).name();
        if (name.equals("None")) {
          name = rule.getSimpleName();
        }
        Object ruleInstance = getRuleInstance(rule);
        if (ruleInstance == null) {
          continue;
        }
        Rule auditableRule = new AuditableRule(new RuleAdapter(ruleInstance), name);
        ruleBook.addRule(auditableRule);
        ((Auditable)auditableRule).setAuditor(this);
      }
      ruleBook.run(facts);
      Optional<Result> result = ruleBook.getResult();
      result.ifPresent(res -> _result.setValue(res.getValue()));
    } catch (IOException | InvalidPathException ex) {
      LOGGER.error("Unable to find rule classes", ex);
    } catch (InstantiationException | IllegalAccessException ex) {
      LOGGER.error("Unable to create an instance of '" + _prototypeClass.getName()
          + "' with the default constructor", ex);
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public void setDefaultResult(Object result) {
    _result = new Result(result);
  }

  @Override
  @SuppressWarnings("unchecked")
  public Optional<Result> getResult() {
    return _result.getValue() == null ? Optional.empty() : Optional.of(_result);
  }

  @Override
  public boolean hasRules() {
    try {
      return getPojoRules().size() > 0;
    } catch (InvalidPathException e) {
      LOGGER.error("Unable to find rule classes", e);
      return false;
    }
  }

  /**
   * Returns a rule instance
   *
   * <p>For container aware contexts (like Spring, Guice, Weld, etc.) override this method and instantiate the rule
   * via the container.
   * @param rule The rule class
   * @return The rule instance
   */
  protected Object getRuleInstance(Class<?> rule) {
    try {
      return rule.newInstance();
    } catch (InstantiationException | IllegalAccessException ex) {
      LOGGER.warn("Unable to create instance of rule using '" + rule + "'", ex);
    }
    return null;
  }

  /**
   * Gets the POJO Rules used in the RuleBook.
   * @return  A List of the POJO Rules used in a RuleBook.
   */
  protected abstract List<Class<?>> getPojoRules();
}


package com.deliveredtechnologies.rulebook;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * RuleBook is an abstract class that follows the Template Method design pattern, leaving the defineRules() method
 * implementation for subclasses. This class facilitates the aggregation and chaining together of Rules.
 */
@Deprecated
public abstract class RuleBook<T> {
  protected Optional<Rule<T>> _headRule = Optional.empty();
  protected Rule<T> _tailRule;
  protected List<Fact<T>> _facts = new ArrayList<>();

  public RuleBook() {
  }

  /**
   * The run() method adds the rules [via defineRules()] and runs the rules as long as at least one rule was
   * added.
   */
  public final void run() {
    if (_headRule.isPresent() == false) {
      defineRules();
    }
    _headRule.ifPresent(rule -> rule.given(_facts));
    _headRule.ifPresent(Rule::run);
  }

  /**
   * The given() method accepts the Facts for this RuleBook.
   * The facts passed in will also be applied to all rules added to this RuleBook.
   *
   * @param facts Facts to be added to the RuleBook and applied to all the Rules in the RuleBook
   * @return the current RuleBook object
   */
  @SafeVarargs
  public final RuleBook<T> given(Fact<T>... facts) {
    Arrays.stream(facts).forEach(_facts::add);
    return this;
  }

  /**
   * The given() method accepts a key/value pair as a Fact for this RuleBook.
   *
   * @param name  the name of the Fact
   * @param value the value of the Fact
   * @return      the current RuleBook object
   */
  public RuleBook<T> given(String name, T value) {
    _facts.add(new Fact<T>(name, value));
    return this;
  }

  /**
   * The addRule() method adds a rule to the end of the Rules chain.
   *
   * @param rule the Rule to be added
   */
  public void addRule(Rule<T> rule) {
    if (rule == null) {
      return;
    }

    if (!_headRule.isPresent()) {
      _headRule = Optional.of(rule); // this rule is the head if there was no head
      _tailRule = rule;
    } else {
      _tailRule.setNextRule(rule);
      _tailRule = rule;
    }
  }

  /**
   * this is where the rules can be specified in the subclass; it will be executed by run().
   */
  protected abstract void defineRules();
}


package com.deliveredtechnologies.rulebook.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * AnnotationUtils is a utility class for annotations.
 */
public final class AnnotationUtils {
  private AnnotationUtils() { }

  /**
   * Method getAnnotatedFields gets the fields annotated of a specific type from the class and its parent classes.

   * The List is in order of closest parent => current obj fields, parent obj fields, etc.
   * @param annotation      the annotation type
   * @param clazz           the class that is expected to be annotated
   * @return                a {@link Set} of {@link Field} objects annotated with the annotatedClass
   */
  @SuppressWarnings("unchecked")
  public static List<Field> getAnnotatedFields(Class annotation, Class clazz) {
    if (clazz == Object.class) {
      return new ArrayList<>();
    }
    List<Field> fields = (List<Field>)Arrays.stream(clazz.getDeclaredFields())
            .filter(field -> field.getAnnotation(annotation) != null)
            .collect(Collectors.toList());
    if (clazz.getSuperclass() != null) {
      fields.addAll(getAnnotatedFields(annotation, clazz.getSuperclass()));
    }
    return fields;
  }

  /**
   * Method getAnnotatedField gets the first annotated field of the type of annotation specified.
   * @param annotation      the type of the annotation
   * @param clazz           the annotated class
   * @return                the first annotated field found in clazz of the type annotation
   */
  public static Optional<Field> getAnnotatedField(Class annotation, Class clazz) {
    List<Field> fields = getAnnotatedFields(annotation, clazz);
    return Optional.ofNullable(fields.size() > 0 ? fields.get(0) : null);
  }

  /**
   * Method getAnnotatedMethods gets the methods annotated of a specific type from the class and its parent
   * classes.
 The List is in order of closest parent => current obj methods, parent obj methods, etc.
   * @param annotation      the type of the annotation
   * @param clazz           the annotated class
   * @return                a List of Methods that have been annotated using annotation in the class clazz
   */
  @SuppressWarnings("unchecked")
  public static List<Method> getAnnotatedMethods(Class annotation, Class clazz) {
    List<Method> methods = new ArrayList<>();
    if (clazz == Object.class) {
      return methods;
    }
    methods.addAll((List<Method>)
        Arrays.stream(clazz.getDeclaredMethods())
        .filter(field -> field.getAnnotation(annotation) != null)
        .collect(Collectors.toList()));
    methods.addAll(getAnnotatedMethods(annotation, clazz.getSuperclass()));
    return methods;
  }

  /**
   * Method getAnnotatedMethod the first annotated method of the type of annotation specified.
   * @param annotation      the type of the annotation
   * @param clazz           the annotated class
   * @return                the first annotated field found in clazz of the type annotation
   */
  public static Optional<Method> getAnnotatedMethod(Class annotation, Class clazz) {
    List<Method> methods = getAnnotatedMethods(annotation, clazz);
    return Optional.ofNullable(methods.size() > 0 ? methods.get(0) : null);
  }

  /**
   * Method getAnnotation returns the annotation on a class or its parent annotation.
   * @param clazz       the annotated class
   * @param annotation  the annotation to find
   * @param          the type of the annotation
   * @return            the actual annotation used or null if it doesn't exist
   */
  @SuppressWarnings("unchecked")
  public static  A getAnnotation(Class annotation, Class<?> clazz) {
    return Optional.ofNullable(clazz.getAnnotation(annotation)).orElse((A)
      Arrays.stream(clazz.getDeclaredAnnotations())
        .flatMap(anno -> Arrays.stream(anno.getClass().getInterfaces())
          .flatMap(iface -> Arrays.stream(iface.getDeclaredAnnotations())))
        .filter(annotation::isInstance)
        .findFirst().orElse(null)
    );
  }
}


package com.deliveredtechnologies.rulebook;

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

import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * StandardDecision is the standard implementation of {@link Decision}.
 */
@Deprecated
public class StandardDecision<T, U> implements Decision<T, U> {
  private static Logger LOGGER = LoggerFactory.getLogger(StandardDecision.class);

  private Rule<T> _rule;

  private Result _result = new Result<>();

  public StandardDecision(Class<T> factClazz, Class resultClazz) {
    _rule = new StandardRule<T>(factClazz);
  }

  /**
   * This create() method is a convenience method to avoid using new and generic syntax.
   *
   * @param factType    the type of object stored in facts for this Decision
   * @param resultType  the type of object stored in the Result
   * @param <T>         the class type of the objects in the Facts used
   * @param          the class type of object stored in the Result
   *
   * @return a new instance of StandardDecision
   */
  public static <T, U> StandardDecision<T, U> create(Class<T> factType, Class resultType) {
    return new StandardDecision<T, U>(factType, resultType);
  }

  /**
   * This create() method is another convenience method to create a non-type specific StandardDecision.
   * @return a new instance of StandardDecision
   */
  public static StandardDecision<Object, Object> create() {
    return new StandardDecision<Object, Object>(Object.class, Object.class);
  }

  /**
   * The run() method runs the {@link Predicate} supplied by the when() method.

   * If it evaluates to true then the {@link BiConsumer}(s) and {@link Consumer}(s) supplied by the then() method(s)
   * are executed.

   * If the stop() method was invoked then no further rules are evaluated.

   * Otherwise, the next rule in the chain is evaluated.
   */
  @Override
  @SuppressWarnings("unchecked")
  public void run(Object... args) {
    _rule.run(_result);
  }

  /**
   * The given() method accepts a name/value pair to be used as a Fact.
   * @param name  name of the Fact
   * @param value object provided as the Fact with the given name
   * @return      the current object
   */
  @Override
  public Decision<T, U> given(String name, T value) {
    _rule.given(name, value);
    return this;
  }

  /**
   * The given() method accepts Facts for the StandardDecision.
   * @param facts Facts to be used by the Rule
   * @return      the current object for chaining other methods
   */
  @Override
  public Decision<T, U> given(Fact<T>... facts) {
    _rule.given(facts);
    return this;
  }

  /**
   * The given() method accepts Facts for the StandardDecision.
   * @param facts a List of Facts to be used by the Rule
   * @return      the current object
   */
  @Override
  public Decision<T, U> given(List<Fact<T>> facts) {
    _rule.given(facts);
    return this;
  }

  /**
   * The given() method accepts Facts for the StandardDecision.
   * @param facts a {@link FactMap}
   * @return      the current object
   */
  @Override
  public Decision<T, U> given(FactMap<T> facts) {
    _rule.given(facts);
    return this;
  }

  @Override
  public Decision<T, U> givenUnTyped(FactMap facts) {
    return null;
  }

  @Override
  public FactMap getFactMap() {
    return _rule.getFactMap();
  }

  /**
   * The when() method accepts a {@link Predicate} that returns true or false based on Facts.
   * @param test  the condition(s) to be evaluated against the Facts
   * @return      true, if the then() statement(s) should be evaluated, otherwise false
   */
  @Override
  public Decision<T, U> when(Predicate<FactMap<T>> test) {
    _rule.when(test);
    return this;
  }

  /**
   * The then() method accepts a {@link BiConsumer} that performs an action based on Facts.

   * The arguments of the BiConsumer are a {@link FactMap} and a {@link Result}, respectively.
   * @param action  the action to be performed
   * @return        the current object
   */
  @Override
  public Decision<T, U> then(BiConsumer<FactMap<T>, Result> action) {
    _rule.getThen().add(action);
    return this;
  }

  /**
   * The then() method accepts a {@link Consumer} that performs an action based on Facts.
   * @param action  the action to be performed
   * @return        the current object
   */
  @Override
  public Decision<T, U> then(Consumer<FactMap<T>> action) {
    _rule.then(action);
    return this;
  }

  /**
   * The stop() method causes the rule chain to stop if the when() condition true and only
   * after the then() actions have been executed.
   * @return  the current object
   */
  @Override
  public Decision<T, U> stop() {
    _rule.stop();
    return this;
  }

  /**
   * The using() method reduces the facts to those specifically named here.
   * The using() method only applies to the then() method immediately following it.
   * @param factNames the names of the facts to be used
   * @return          the current object
   */
  @Override
  @SuppressWarnings("unchecked")
  public Decision<T, U> using(String... factNames) {
    _rule.using(factNames);
    return this;
  }

  /**
   * The setNextRule() method sets the next rule in the chain.
   * @param rule  the next Rule to add to the chain
   */
  @Override
  public void setNextRule(Rule<T> rule) {
    _rule.setNextRule(rule);
  }

  /**
   * The getResult() method gets the stored Result value from the execution of the StandardDecision.
   * @return the stored Result value
   */
  @Override
  public U getResult() {
    return _result.getValue();
  }

  /**
   * The setResult() method sets the stored Result.
   * @param result the instantiated result
   */
  @Override
  public void setResult(Result result) {
    _result = result;
  }

  /**
   * The getWhen() method returns the {@link Predicate} to be used for the condition of the Rule.
   * @return  the Predicate used for the condition
   */
  @Override
  public Predicate<FactMap<T>> getWhen() {
    return _rule.getWhen();
  }

  /**
   * The getThen() method returns a {@link List} of {@link Consumer} objects that combined
   * together in sequence represent the then() action(s).
   * @return  a List of Consumer and/or BiConsumer objects
   */
  @Override
  public List<Object> getThen() {
    return _rule.getThen();
  }
}


package com.deliveredtechnologies.rulebook.model;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

/**
 * Auditors are used for auditing rules. They maintain a record of each Rule and their state.
 */
public abstract class Auditor {
  private Map<String, Map<Long, RuleStatus>> _auditMap = new HashMap<>();
  private ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();

  /**
   * Registers a rule to be audited.
   * @param rule  and {@link Auditable} rule
   */
  public void registerRule(Auditable rule) {
    _lock.writeLock().lock();
    try {
      _auditMap.put(rule.getName(), new HashMap<>());
    } finally {
      _lock.writeLock().unlock();
    }
  }

  /**
   * Updates the status of the rule & stores the status with the Auditor.
   * @param rule    the rule in question
   * @param status  the status of the rule
   */
  public void updateRuleStatus(Auditable rule, RuleStatus status) {
    _lock.readLock().lock();
    try {
      if (_auditMap.containsKey(rule.getName())) {
        _lock.readLock().unlock();
        _lock.writeLock().lock();
        try {
          _auditMap.get(rule.getName()).put(Thread.currentThread().getId(), status);
          _lock.readLock().lock();
        } finally {
          _lock.writeLock().unlock();
        }
      }
    } finally {
      _lock.readLock().unlock();
    }
  }

  /**
   * Gets the status of the rule with the given name.
   * @param name  the name of the rule
   * @return      the status of the rule
   */
  public RuleStatus getRuleStatus(String name) {
    return getRuleStatusMap().getOrDefault(name, RuleStatus.NONE);
  }

  /**
   * Gets a map of each rule name with its associated status.
   * @return  a map of rule names and their associated status
   */
  public Map<String, RuleStatus> getRuleStatusMap() {
    _lock.readLock().lock();
    try {
      return _auditMap.keySet().stream()
          .collect(
              Collectors.toMap(key -> key,
                  key -> _auditMap.get(key).getOrDefault(Thread.currentThread().getId(), RuleStatus.PENDING)));
    } finally {
      _lock.readLock().unlock();
    }
  }
}


package com.deliveredtechnologies.rulebook.lang;

import com.deliveredtechnologies.rulebook.model.Auditor;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.RuleBook;
import com.deliveredtechnologies.rulebook.model.RuleBookAuditor;
import com.deliveredtechnologies.rulebook.model.rulechain.cor.CoRRuleBook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.function.Consumer;

/**
 * The initial builder used to build a RuleBook.
 */
public class RuleBookBuilder<T> implements TerminatingRuleBookBuilder<T> {

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

  private RuleBook<T> _ruleBook;
  private Class<? extends RuleBook> _ruleBookClass;
  private Class<?> _resultType = Object.class;

  /**
   * Returns a new RuleBookBuilder for the default RuleBook type.
   * @return  a new RuleBookBuilder
   */
  public static RuleBookBuilder<Object> create() {
    return new RuleBookBuilder<Object>(CoRRuleBook.class);
  }

  /**
   * Returns a new RuleBookBuilder using the supplied RuleBook class.
   * @param ruleBookClass a RuleBook class to be used in the builder
   * @return              a new RuleBookBuilder
   */
  public static RuleBookBuilder<Object> create(Class<? extends RuleBook> ruleBookClass) {
    return new RuleBookBuilder<Object>(ruleBookClass);
  }

  private RuleBookBuilder(Class<? extends RuleBook> ruleBookClass) {
    _ruleBookClass = ruleBookClass;
  }

  @SuppressWarnings("unchecked")
  private RuleBookBuilder(RuleBookBuilder ruleBookBuilder) {
    _resultType = ruleBookBuilder._resultType;
    _ruleBookClass = ruleBookBuilder._ruleBookClass;
    newRuleBook();
  }

  /**
   * Decorates the RuleBook with {@link Auditor} functionality for rules auditing.
   * @return  a builder that can add rules
   */
  public RuleBookAddRuleBuilder<T> asAuditor() {
    return new RuleBookAddRuleBuilder<>(new RuleBookAuditor<>(newRuleBook()));
  }

  /**
   * Specifies the Result type for the RuleBook.
   * @param resultType  result class
   * @param <U>         type of the result class
   * @return            a builder with the new Result type
   */
  public <U> RuleBookWithResultTypeBuilder<U> withResultType(Class<U> resultType) {
    _resultType = resultType;
    return new RuleBookWithResultTypeBuilder<U>((new RuleBookBuilder<U>(this)).newRuleBook());
  }

  /**
   * Adds a rule to the RuleBook.
   * @param consumer  functional interface that supplies a RuleBookRuleBuilder for building a Rule
   * @return          a builder with the added Rule
   */
  public RuleBookAddRuleBuilder<T> addRule(Consumer<RuleBookRuleBuilder<T>> consumer) {
    return new RuleBookAddRuleBuilder<>(newRuleBook(), consumer);
  }

  /**
   * Adds a rule to the RuleBook.
   * @param rule  Rule to be added into the RuleBook
   * @param <U>   the fact type of the Rule
   * @return      RuleBookBuilder with the added Rule
   */
  public <U> RuleBookAddRuleBuilder<T> addRule(Rule<U, T> rule) {
    return new RuleBookAddRuleBuilder<>(newRuleBook(), rule);
  }

  /**
   * Builds the RuleBook.
   * @return  a RuleBook
   */
  @Override
  public RuleBook<T> build() {
    return (new RuleBookBuilder<T>(_ruleBookClass)).newRuleBook();
  }

  private RuleBook<T> newRuleBook() {
    if (_ruleBook == null) {
      try {
        _ruleBook = _ruleBookClass.newInstance();
      } catch (IllegalAccessException | InstantiationException e) {
        try {
          Constructor<?> constructor = _ruleBookClass.getConstructor(Class.class);
          _ruleBook = (RuleBook<T>) constructor.newInstance(_resultType);
        } catch (InvocationTargetException
            | NoSuchMethodException
            | InstantiationException
            | IllegalAccessException ex) {
          throw new IllegalStateException("RuleBook of class " + _ruleBookClass + " can not be instantiated", ex);
        }
      }
    }
    return _ruleBook;
  }
}


package com.deliveredtechnologies.rulebook;

import com.deliveredtechnologies.rulebook.util.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.deliveredtechnologies.rulebook.RuleState.BREAK;
import static com.deliveredtechnologies.rulebook.RuleState.NEXT;

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

/**
 * StandardRule is a standard rule implementation that can be used with a {@link RuleBook}.
 */
@Deprecated
public class StandardRule<T> implements Rule<T> {
  private static Logger LOGGER = LoggerFactory.getLogger(StandardRule.class);

  private Class<T> _factType;
  private Optional<Rule<T>> _nextRule = Optional.empty();
  private FactMap _facts = new FactMap();
  private Predicate<FactMap<T>> _test;
  private List<Object> _actionChain = new ArrayList<>();
  private Map<Integer, List<String>> _factNameMap = new HashMap<>();
  private RuleState _ruleState = NEXT;

  /**
   * This create() method is a convenience method to avoid using new and generic syntax.
   *
   * @param factType the type of object stored in facts for this Rule
   * @param <T>      the type of Fact
   * @return a new instance of a StandardRule
   */
  public static <T> StandardRule<T> create(Class<T> factType) {
    return new StandardRule<>(factType);
  }

  /**
   * This create() method is a convenience method to create a non-type specific StandardRule.
   * @return a new instance of a StandardRule
   */
  public static StandardRule<Object> create() {
    return new StandardRule<>(Object.class);
  }

  protected StandardRule() {
  }

  public StandardRule(Class<T> clazz) {
    this();
    _factType = clazz;
  }

  /**
   * The run(Object[]) method runs the {@link Predicate} supplied by the when() method.

   * If it evaluates to true then the action(s) supplied by the then() method(s) are executed.

   * If the stop() method was invoked then no further rules are evaluated.

   * Otherwise, the next rule in the chain is evaluated.
   */
  @Override
  @SuppressWarnings("unchecked")
  public void run(Object... otherArgs) {
    try {
      //only use facts of the specified type
      FactMap<T> typeFilteredFacts = new FactMap<T>((Map<String, NameValueReferable<T>>)_facts.values().stream()
          .filter((Object fact) -> _factType.isAssignableFrom(((Fact)fact).getValue().getClass()))
          .collect(Collectors.toMap(fact -> ((Fact)fact).getName(), fact -> (Fact<T>)fact)));
      //invoke then() action(s) if when() is true or if when() was never specified
      if (getWhen() == null || getWhen().test(typeFilteredFacts)) {

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

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

          //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[]{usingFacts}, otherArgs, method.getParameterCount()));
                } catch (IllegalAccessException | InvocationTargetException err) {
                  LOGGER.error("Error invoking action on " + action.getClass(), err);
                }
              });
        }

        //if stop() was invoked, stop the rule chain after then is finished executing
        if (_ruleState == BREAK) {
          return;
        }
      }
    } catch (Exception ex) {

      //catch errors in case something like one rule was chained expecting a Fact that doesn't exist
      //eventually, we'll have to resolve that kind of issue ahead of time
      LOGGER.error("Error occurred when trying to evaluate rule!", ex);
    }

    //continue down the rule chain
    _nextRule.ifPresent(rule -> rule.given(_facts));
    _nextRule.ifPresent(Rule::run);
  }

  /**
   * The given() method accepts a name/value pair to be used as a Fact.
   * @param name  name of the Fact
   * @param value object provided as the Fact with the given name
   * @return      the current object
   */
  @Override
  @SuppressWarnings("unchecked")
  public Rule<T> given(String name, T value) {
    _facts.put(name, new Fact<T>(name, value));
    return this;
  }

  /**
   * The given() method accepts Facts to be evaluated in the Rule.
   * @param facts Facts to be used by the Rule
   * @return      the current object
   */
  @Override
  @SuppressWarnings("unchecked")
  public Rule<T> given(Fact<T>... facts) {
    for (Fact f : facts) {
      _facts.put(f.getName(), f);
    }

    return this;
  }

  /**
   * The given() method accepts Facts to be evaluated in the Rule.
   * @param facts a List of Facts to be used by the Rule
   * @return      the current object
   */
  @SuppressWarnings("unchecked")
  @Override
  public Rule<T> given(List<Fact<T>> facts) {
    for (Fact f : facts) {
      _facts.put(f.getName(), f);
    }

    return this;
  }

  /**
   * The given() method accepts Facts to be evaluated in the Rule.
   * @param facts a {@link FactMap}
   * @return      the current object
   */
  @Override
  public Rule<T> given(FactMap<T> facts) {
    _facts = facts;
    return this;
  }

  @Override
  public Rule<T> givenUnTyped(FactMap facts) {
    _facts = facts;
    return this;
  }

  @Override
  public FactMap getFactMap() {
    return _facts;
  }

  /**
   * The when() method accepts a {@link Predicate} that returns true or false based on Facts.
   * @param test  the condition(s) to be evaluated against the Facts
   * @return      the current object
   */
  @Override
  public Rule<T> when(Predicate<FactMap<T>> test) {
    _test = test;
    return this;
  }

  /**
   * The then() method accepts a {@link Consumer} that performs an action based on Facts.
   *
   * @param action  the action to be performed
   * @return        the current object
   */
  @Override
  @SuppressWarnings("unchecked")
  public Rule<T> then(Consumer<FactMap<T>> action) {
    _actionChain.add(action);
    return this;
  }

  /**
   * The stop() method causes the rule chain to stop if the when() condition true and only
   * after the then() actions have been executed.
   * @return  the current object
   */
  @Override
  public Rule<T> stop() {
    _ruleState = BREAK;
    return this;
  }

  /**
   * The using() method reduces the facts to those specifically named here.
   * The using() method only applies to the then() method immediately following it.
   * @param factNames the names of the facts to be used
   * @return          the current object
   */
  @Override
  @SuppressWarnings("unchecked")
  public StandardRule<T> using(String... factNames) {
    List<String> factNameList = Stream.of(factNames)
        .filter(name -> _factType.isInstance(_facts.getValue(name)))
        .collect(Collectors.toList());
    if (_factNameMap.containsKey((getThen()).size())) {
      List<String> existingFactNames = _factNameMap.get((getThen()).size());
      existingFactNames.addAll(factNameList);
      return this;
    }

    _factNameMap.put((getThen()).size(), factNameList);
    return this;
  }

  /**
   * The setNextRule() method sets the next Rule in the chain.
   * @param rule the next Rule to add to the chain
   */
  @Override
  public void setNextRule(Rule<T> rule) {
    _nextRule = Optional.ofNullable(rule);
  }

  /**
   * The getWhen() method returns the {@link Predicate} to be used for the condition of the Rule.
   * @return  the Predicate condition
   */
  @Override
  public Predicate<FactMap<T>> getWhen() {
    return _test;
  }

  /**
   * The getThen() method returns a {@link List} of {@link Consumer} objects that combined
   * together in sequence represent the then() action(s).
   * @return  a List of Consumer objects
   */
  @Override
  public List<Object> getThen() {
    return _actionChain;
  }
}


package com.deliveredtechnologies.rulebook;

import java.util.Optional;

/**
 * DecisionBook is a type of {@link RuleBook} that stores a return type linked to all the rules in the DecisionBook.
 */
@Deprecated
public abstract class DecisionBook<T, U> extends RuleBook<T> {
  private Result<U> _result = new Result<U>();

  /**
   * The withDefaultResult method allows a default result value to be specified.
   * When using the DSL syntax to chain calls, this method should be the first one specified.
   *
   * @param result the initial value of the stored result
   * @return the current DecisionBook object
   */
  public final DecisionBook<T, U> withDefaultResult(U result) {
    _result.setValue(result);
    return this;
  }

  /**
   * The addRule() method allows a rule to be added to the DecisionBook in the abstract defineRules
   * method.
   * @param rule  the Decision rule to be added to the DecisionBook
   */
  public void addRule(Decision<T, U> rule) {
    if (rule == null) {
      return;
    }

    super.addRule(rule);
    rule.setResult(_result);
  }

  /**
   * The getResult() method allows the result of the DecisionBook rules execution to be retrieved.
   * @return the stored result value
   */
  public U getResult() {
    return _result.getValue();
  }

}


package com.deliveredtechnologies.rulebook.lang;

import com.deliveredtechnologies.rulebook.FactMap;
import com.deliveredtechnologies.rulebook.NameValueReferableTypeConvertibleMap;
import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.RuleState;
import com.deliveredtechnologies.rulebook.model.Rule;

import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
 * Builds the portion of a Rule that is available in the languate on 'then'.
 */
public class ThenRuleBuilder<T, U> implements TerminatingRuleBuilder<T, U> {
  Rule<T, U> _rule;

  ThenRuleBuilder(Rule<T, U> rule, Consumer<NameValueReferableTypeConvertibleMap<T>> action) {
    _rule = rule;
    _rule.addAction(action);
  }

  ThenRuleBuilder(Rule<T, U> rule, BiConsumer<NameValueReferableTypeConvertibleMap<T>, Result<U>> action) {
    _rule = rule;
    _rule.addAction(action);
  }

  /**
   * Adds a using constraint in the Rule that restricts the facts supplied to the subsequent 'then' action.
   * @param factNames the fact names to be supplied to the subsequent 'then' action
   * @return          a builder the allows for the Rule to be built following the 'using' statement
   */
  public UsingRuleBuilder<T, U> using(String... factNames) {
    return new UsingRuleBuilder<T, U>(_rule, factNames);
  }

  /**
   * Adds a then action into the Rule.
   * @param action  an action that the rule will execute based on the condition
   * @return        a builder that allows for the Rule to be built following the 'then' statement
   */
  public ThenRuleBuilder<T, U> then(Consumer<NameValueReferableTypeConvertibleMap<T>> action) {
    _rule.addAction(action);
    return this;
  }

  /**
   * Addds a then action into the Rule.
   * @param action  an action that the rule will execute based on the condition; accepts facts and the result
   * @return        a builder that allows for the Rule to be built following the 'then' statement
   */
  public ThenRuleBuilder<T, U> then(BiConsumer<NameValueReferableTypeConvertibleMap<T>, Result<U>> action) {
    _rule.addAction(action);
    return this;
  }

  /**
   * Sends a signal that the rule chain should be broken.
   * @return  a TerminatingRuleBuilder that only allows for the Rule to be built following stop()
   */
  public TerminatingRuleBuilder<T, U> stop() {
    _rule.setRuleState(RuleState.BREAK);
    return this;
  }

  /**
   * Builds the Rule.
   * @return  a Rule
   */
  public Rule<T, U> build() {
    return _rule;
  }

}


package com.deliveredtechnologies.rulebook.lang;

import com.deliveredtechnologies.rulebook.model.RuleBook;

/**
 * Builds the porition of the RuleBook that specifies the default result type.
 */
public class RuleBookWithResultTypeBuilder<T> {

  private RuleBook<T> _ruleBook;

  public RuleBookWithResultTypeBuilder(RuleBook<T> ruleBook) {
    _ruleBook = ruleBook;
  }

  /**
   * Specifies the default Result value.
   * Note: RuleBooks that return a single Result must have a default Result value set.
   * @param result  the default value of the Result
   * @return        a builder with the default Result value
   */
  public RuleBookDefaultResultBuilder<T> withDefaultResult(T result) {
    _ruleBook.setDefaultResult(result);
    return new RuleBookDefaultResultBuilder<>(_ruleBook);
  }
}


package com.deliveredtechnologies.rulebook;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * This is a wrapper class for the actual result.
 * This is needed to ensure that an instance is available and that a reference is maintained to the actual that
 * can be manipulated in the then {@link java.util.function.BiFunction}.
 * The Result object can then contain a pointer to any instance, which can be shared with lamda calls
 * and across other objects (i.e. DecisionBook and Decision objects)
 */
public class Result<T> implements Referable<T> {
  private Map<Long, T> _valueMap = new HashMap<>();
  private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();
  private T _defaultValue = null;

  public Result() {}

  /**
   * Creates an instance of Result with a default value.
   *
   * @param value the default value.
   */
  public Result(T value) {
    _defaultValue = value;
  }

  /**
   * Resets the value of the Result to its default value.
   */
  public void reset() {
    _lock.readLock().lock();
    try {
      if (_defaultValue == null) {
        return;
      }
    } finally {
      _lock.readLock().unlock();
    }
    setValue(_defaultValue);
  }

  /**
   * The method getValue() returns the object contained in the Result object.
   *
   * @return the object stored in the Result object
   */
  @Override
  public T getValue() {
    _lock.readLock().lock();
    try {
      long key = Thread.currentThread().getId();
      if (_valueMap.containsKey(key)) {
        return _valueMap.get(Thread.currentThread().getId());
      }
      return _defaultValue;
    } finally {
      _lock.readLock().unlock();
    }
  }

  /**
   * The method setValue() sets the object to be contained in the Result object.
   *
   * @param value the object to be stored in the Result object
   */
  @Override
  public void setValue(T value) {
    _lock.writeLock().lock();
    try {
      _valueMap.put(Thread.currentThread().getId(), value);
    } finally {
      _lock.writeLock().unlock();
    }
  }

  @Override
  public String toString() {
    _lock.readLock().lock();
    try {
      long key = Thread.currentThread().getId();
      if (_valueMap.containsKey(key)) {
        return _valueMap.get(key).toString();
      }
      if (_defaultValue != null) {
        return _defaultValue.toString();
      }
      return "";
    } finally {
      _lock.readLock().unlock();
    }
  }
}


package com.deliveredtechnologies.rulebook.lang;

import com.deliveredtechnologies.rulebook.model.Auditor;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.RuleBook;
import com.deliveredtechnologies.rulebook.model.RuleBookAuditor;

import java.util.function.Consumer;

/**
 * Builds the portion of the RuleBook that specifies a default result value.
 */
public class RuleBookDefaultResultBuilder<T> implements TerminatingRuleBookBuilder<T> {

  private RuleBook<T> _ruleBook;

  public RuleBookDefaultResultBuilder(RuleBook<T> ruleBook) {
    this._ruleBook = ruleBook;
  }

  /**
   * Adds a rule to the RuleBook.
   * @param consumer  functional interface that supplies a RuleBookRuleBuilder for building a Rule
   * @return          a builder with the added Rule
   */
  public RuleBookAddRuleBuilder<T> addRule(Consumer<RuleBookRuleBuilder<T>> consumer) {
    return new RuleBookAddRuleBuilder<>(_ruleBook, consumer);
  }

  /**
   * Adds a rule to the RuleBook.
   * @param rule  Rule to be added into the RuleBook
   * @param <U>   the fact type of the Rule
   * @return      RuleBookBuilder with the added Rule
   */
  public <U> RuleBookAddRuleBuilder<T> addRule(Rule<U, T> rule) {
    return new RuleBookAddRuleBuilder<>(_ruleBook, rule);
  }

  /**
   * Decorates the RuleBook with {@link Auditor} functionality for rules auditing.
   * @return  a builder that can add rules
   */
  public RuleBookAddRuleBuilder<T> asAuditor() {
    return new RuleBookAddRuleBuilder<>(new RuleBookAuditor<>(_ruleBook));
  }

  @Override
  public RuleBook<T> build() {
    return _ruleBook;
  }
}


package com.deliveredtechnologies.rulebook.lang;

import com.deliveredtechnologies.rulebook.model.GoldenRule;
import com.deliveredtechnologies.rulebook.model.Rule;
import com.deliveredtechnologies.rulebook.model.RuleBook;

/**
 * Chains building of a Rule onto a RuleBookBuilder.
 * @param <U> the type of the Result
 */
public class RuleBookRuleBuilder<U> {
  private RuleBook<U> _ruleBook;
  private Class<? extends Rule> _ruleClass = GoldenRule.class;

  RuleBookRuleBuilder(RuleBook<U> ruleBook) {
    _ruleBook = ruleBook;
  }

  /**
   * Specifies the Rule type.
   * @param ruleType  the type (class) of Rule to build
   * @return          RuleBookRuleBuilder using the Rule type specified
   */
  public RuleBookRuleBuilder<U> withRuleType(Class<? extends Rule> ruleType) {
    _ruleClass = ruleType;
    return this;
  }

  public RuleBookAuditableRuleBuilder<U> withName(String ruleName) {
    return new RuleBookAuditableRuleBuilder<>(_ruleBook, _ruleClass, ruleName);
  }

  /**
   * Specifies the fact type
   * @param factType  the type of object that facts are restricted to in the Rule.
   * @param <T>       the generic fact type
   * @return          RuleBookRuleWithFactTypeBuilder for building RuleBook Rules with a specific fact type
   */
  @SuppressWarnings("unchecked")
  public <T> RuleBookRuleWithFactTypeBuilder<T, U> withFactType(Class<T> factType) {
    Rule<T, U> rule = (Rule<T, U>)RuleBuilder.create(_ruleClass).withFactType(factType).build();
    _ruleBook.addRule(rule);
    return new RuleBookRuleWithFactTypeBuilder<>(rule);
  }

  /**
   * Specifies no fact type.
   * A generic Object type is used for the Rule's fact type.
   * @return RuleBookRuleWithFactTypeBuilder for building RuleBook Rules with a specific fact type
   */
  public RuleBookRuleWithFactTypeBuilder<Object, U> withNoSpecifiedFactType() {
    return withFactType(Object.class);
  }
}


package com.deliveredtechnologies.rulebook.model.runner;

import com.deliveredtechnologies.rulebook.Result;
import com.deliveredtechnologies.rulebook.model.RuleBook;
import com.deliveredtechnologies.rulebook.model.rulechain.cor.CoRRuleBook;

import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.deliveredtechnologies.rulebook.util.AnnotationUtils.getAnnotation;
import static java.util.Comparator.comparingInt;

/**
 * Runs the POJO Rules in a specified package as a RuleBook.
 */
public class RuleBookRunner extends AbstractRuleBookRunner {

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

  private String _package;
  private Predicate<String> _subPkgMatch;
  private Class<? extends RuleBook> _prototypeClass;

  @SuppressWarnings("unchecked")
  private Result _result = new Result(null);

  /**
   * Creates a new RuleBookRunner using the specified package and the default RuleBook.
   * @param rulePackage a package to scan for POJO Rules
   */
  public RuleBookRunner(String rulePackage) {
    this(rulePackage, s -> s.startsWith(rulePackage));
  }

  /**
   * Creates a new RuleBookRunner using the specified package and the default RuleBook.
   * @param rulePackage a package to scan for POJO Rules
   * @param subPkgMatch Predicate that returns true for any subpackages that will be scanned
   */
  public RuleBookRunner(String rulePackage, Predicate<String> subPkgMatch) {
    this(CoRRuleBook.class, rulePackage, subPkgMatch);
  }

  /**
   * Creates a new RuleBookRunner using the specified package and the supplied RuleBook.
   * @param ruleBookClass the RuleBook type to use as a delegate for the RuleBookRunner
   * @param rulePackage   the package to scan for POJO rules
   */
  public RuleBookRunner(Class<? extends RuleBook> ruleBookClass, String rulePackage) {
    this(ruleBookClass, rulePackage, s -> s.startsWith(rulePackage));
  }

  /**
   * Creates a new RuleBookRunner using the specified package, the supplied RuleBook and matching subpackages Predicate.
   * @param ruleBookClass the RuleBook type to use as a delegate for the RuleBookRunner
   * @param rulePackage   the package to scan for POJO rules
   * @param subPkgMatch   Predicate that returns true for any subpackages that will be scanned
   */
  public RuleBookRunner(Class<? extends RuleBook> ruleBookClass, String rulePackage, Predicate<String> subPkgMatch) {
    super(ruleBookClass);
    _prototypeClass = ruleBookClass;
    _package = rulePackage;
    _subPkgMatch = subPkgMatch;
  }

  /**
   * Gets the POJO Rules to be used by the RuleBook via reflection of the specified package.
   * @return  a List of POJO Rules
   */
  protected List<Class<?>> getPojoRules() {
    Reflections reflections = new Reflections(_package);

    List<Class<?>> rules = reflections
        .getTypesAnnotatedWith(com.deliveredtechnologies.rulebook.annotation.Rule.class).stream()
        .filter(rule -> rule.getAnnotatedSuperclass() != null) // Include classes only, exclude interfaces, etc.
        .filter(rule -> _subPkgMatch.test(rule.getPackage().getName()))
        .collect(Collectors.toList());

    rules.sort(comparingInt(aClass ->
        getAnnotation(com.deliveredtechnologies.rulebook.annotation.Rule.class, aClass).order()));

    return rules;
  }
}


package com.deliveredtechnologies.rulebook.util;

import java.lang.reflect.Array;
import java.util.List;
import java.util.stream.Stream;

/**
 * ArrayUtils is a utility class for arrays.
 */
public final class ArrayUtils {

  private ArrayUtils() { }

  /**
   * The combine() static method combines the contents of two arrays into a single array of the same type.
   * @param array1  the array to be concatenated on the left
   * @param array2  the array to be concatenated on the right
   * @param <T>     the type of array
   * @return        a concatenation of array1 and array2
   */
  @SuppressWarnings("unchecked")
  public static <T> T[] combine(T[] array1, T[] array2) {
    return combine(array1, array2, array1.length + array2.length);
  }

  /**
   * The combine() static method combines the contents of two arrays into a single array of the same type that contains
   * no more than the maxElements number of elements.
   * @param array1      the array to be concatenated on the left
   * @param array2      the array to be concatenated on the right
   * @param maxElements the maximum number of elements in the resulting array
   * @param <T>         the type of array
   * @return            a concatenation of array1 and array2
   */
  @SuppressWarnings("unchecked")
  public static <T> T[] combine(T[] array1, T[] array2, int maxElements) {
    if (array1.length == maxElements) {
      return array1;
    } else if (array1.length > maxElements) {
      T[] combinedArray = (T[])Array.newInstance(array1.getClass().getComponentType(), maxElements);
      System.arraycopy(array1, 0, combinedArray, 0, maxElements);
      return combinedArray;
    }

    maxElements = array1.length + array2.length >= maxElements ? maxElements : array1.length + array2.length;
    T[] combinedArray = (T[])Array.newInstance(array1.getClass().getComponentType(), maxElements);
    System.arraycopy(array1, 0, combinedArray, 0, array1.length);
    System.arraycopy(array2, 0, combinedArray, array1.length, maxElements - array1.length);
    return combinedArray;
  }
}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.core.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * AsciiDoc utils for parsing specific elements coded in AsciiDoc format.
 */
public class AsciiDocParser {

    private static final String END_TABLE = "|===";

    /**
     *
     * This method serializes the content of an AsciiDoc table used to set the mapping between kinds and filenames.
     * These tables are of form:
     *
     * <pre>
     * [cols=2*,options="header"]
     * |===
     * |Kind
     * |File type
     *
     * a|ConfigMap
     * a|`cm`
     *
     * |CronJob
     * |cronjob
     * |===
     * </pre>
     *
     * Basically the table contains an optional attributes section (@code{[]}), then the headers of the columns.
     * Between line breaks, both columns representing the kind (first column) and filename (second column).
     * Notice that one-line columns definition @code{|Cell in column 1, row 1|Cell in column 1, row 1} is not supported.
     *
     * This method returns an @code{IllegalArgumentException} if does not contain two columns.
     *
     * @param table definition in AsciiDoc format.
     * @return A serialization of all columns.
     */
    public Map<String, List<String>> serializeKindFilenameTable(final InputStream table) {

        final Map<String, List<String>> serializedContent = new HashMap<>();

        try (final BufferedReader tableContent = new BufferedReader(new InputStreamReader(table))) {

            skipUntilColumns(tableContent);

            boolean endTable = false;

            while (!endTable) {
                final List<String> readRow = readRow(tableContent);
                final String separator = readEmptyLineOrEndTable(tableContent);

                String kind = readRow.get(0);
                serializedContent.put(kind, readRow.subList(1, readRow.size()));

                if (END_TABLE.equals(separator)) {
                    endTable = true;
                }

            }

        } catch (IOException e) {
            throw new IllegalStateException(e);
        }

        return serializedContent;

    }

    private List<String> readRow(final BufferedReader tableContent) throws IOException {
        final List<String> content = new ArrayList<>();

        final String firstColumn = readColumn(tableContent);
        final String secondColumn = readColumn(tableContent);

        content.add(firstColumn);
        final String[] filenameTypes = secondColumn.split(",");

        for (String filenameType : filenameTypes) {
            content.add(filenameType.trim());
        }

        return content;
    }

    private String readColumn(final BufferedReader tableContent) throws IOException {
        final String column = tableContent.readLine();

        if(column == null || column.isEmpty()) {
            throw new IllegalArgumentException("Trying to read a column but white line or EOF was found.");
        }

        int separator;
        if ((separator = column.indexOf("|")) < 0) {
            throw new IllegalArgumentException(String.format("Expected the initial of a column with (|) but %s found.", column));
        }

        return column.trim().substring(separator + 1)
            .replaceAll("[`_*]", "")
            .trim();

    }

    /**
     * Reads empty line or throw an exception if a none empty line was found.
     */
    private String readEmptyLineOrEndTable(final BufferedReader tableContent) throws IOException {
        final String column = tableContent.readLine();

        if (column != null && column.startsWith(END_TABLE)) {
            return END_TABLE;
        }

        if(column == null || !column.isEmpty()) {
            throw new IllegalArgumentException(String.format("Trying to read an empty line for end of row, but content %s was found or EOF", column));
        }

        return "";
    }

    /**
     * Moves buffer until it finds the first content column (skipping headers).
     * @param tableContent
     */
    private void skipUntilColumns(final BufferedReader tableContent) throws IOException {
        String line;
        while ((line = tableContent.readLine()) != null) {
            if(line.trim().isEmpty()){
                break;
            }
        }
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.core.handler;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.maven.core.config.ResourceConfig;
import io.fabric8.maven.core.util.kubernetes.KubernetesHelper;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.util.ImageName;
import io.fabric8.openshift.api.model.DeploymentConfig;
import io.fabric8.openshift.api.model.DeploymentConfigBuilder;
import io.fabric8.openshift.api.model.DeploymentConfigSpec;
import io.fabric8.openshift.api.model.DeploymentConfigSpecBuilder;

public class DeploymentConfigHandler {
    private final PodTemplateHandler podTemplateHandler;
    DeploymentConfigHandler(PodTemplateHandler podTemplateHandler) {
        this.podTemplateHandler = podTemplateHandler;
    }

    public DeploymentConfig getDeploymentConfig(ResourceConfig config,
                                                List<ImageConfiguration> images, Long openshiftDeployTimeoutSeconds, Boolean imageChangeTrigger, Boolean enableAutomaticTrigger, Boolean isOpenshiftBuildStrategy, List<String> generatedContainers) {

        DeploymentConfig deploymentConfig = new DeploymentConfigBuilder()
                .withMetadata(createDeploymentConfigMetaData(config))
                .withSpec(createDeploymentConfigSpec(config, images, openshiftDeployTimeoutSeconds, imageChangeTrigger, enableAutomaticTrigger, isOpenshiftBuildStrategy, generatedContainers))
                .build();

        return deploymentConfig;
    }

    // ===========================================================

    private ObjectMeta createDeploymentConfigMetaData(ResourceConfig config) {
        return new ObjectMetaBuilder()
                .withName(KubernetesHelper.validateKubernetesId(config.getControllerName(), "controller name"))
                .build();
    }

    private DeploymentConfigSpec createDeploymentConfigSpec(ResourceConfig config, List<ImageConfiguration> images, Long openshiftDeployTimeoutSeconds, Boolean imageChangeTrigger, Boolean enableAutomaticTrigger, Boolean isOpenshiftBuildStrategy, List<String> generatedContainers) {
        DeploymentConfigSpecBuilder specBuilder = new DeploymentConfigSpecBuilder();

        PodTemplateSpec podTemplateSpec = podTemplateHandler.getPodTemplate(config,images);

        specBuilder.withReplicas(config.getReplicas())
                .withTemplate(podTemplateSpec)
                .addNewTrigger().withType("ConfigChange").endTrigger();

        if (openshiftDeployTimeoutSeconds != null && openshiftDeployTimeoutSeconds > 0) {
            specBuilder.withNewStrategy().withType("Rolling").
                    withNewRollingParams().withTimeoutSeconds(openshiftDeployTimeoutSeconds).endRollingParams().endStrategy();
        }

        return specBuilder.build();
    }

    private void validateContainer(Container container) {
        if (container.getImage() == null) {
            throw new IllegalArgumentException("Container " + container.getName() + " has no Docker image configured. " +
                    "Please check your Docker image configuration (including the generators which are supposed to run)");
        }
    }
}