Skip to content

Instantly share code, notes, and snippets.

@warmuuh
Last active November 6, 2019 10:23
Show Gist options
  • Save warmuuh/1fd2e6ecfabe46bca6644d78b80301cb to your computer and use it in GitHub Desktop.
Save warmuuh/1fd2e6ecfabe46bca6644d78b80301cb to your computer and use it in GitHub Desktop.
package core.utils.easyrules;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.jeasy.rules.annotation.Action;
import org.jeasy.rules.annotation.Condition;
import org.jeasy.rules.annotation.Fact;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
import static java.lang.String.format;
/**
* this class is caching all reflective access to rules defined via annotations
* the jeasy ruleProxy does reflective access on-demand which is slower
*
* @author pmucha
*
*/
@Slf4j
@EqualsAndHashCode
public class CachedReflectiveRule implements Rule {
@Getter
private final long id;
@Getter
private final String description;
@Getter
private final String name;
@Getter
private final int priority;
private final int delegateHashcode;
@EqualsAndHashCode.Exclude
private final InvocationDetails condition;
@EqualsAndHashCode.Exclude
private final List<InvocationDetails> actions;
public static Rule asRule(long id, Object obj) {
if (obj instanceof Rule) {
return (Rule)obj;
}
return new CachedReflectiveRule(id, obj);
}
private CachedReflectiveRule(long id, Object delegate) {
this.id = id;
org.jeasy.rules.annotation.Rule annotation = delegate.getClass().getAnnotation(org.jeasy.rules.annotation.Rule.class);
if (annotation == null) {
throw new RuleProcessingException("Cannot find @Rule annotation on " + delegate.getClass().getName());
}
this.name = annotation.name();
this.description = annotation.description();
this.priority = annotation.priority();
Method conditionMethod = getConditionMethod(delegate);
this.condition = new InvocationDetails(delegate, conditionMethod);
this.actions = getActionMethodBeans(delegate).stream()
.map(a -> new InvocationDetails(delegate, a.getMethod()))
.collect(Collectors.toList());
delegateHashcode = delegate.hashCode();
}
@Override
public boolean evaluate(Facts facts) {
try {
return (Boolean) condition.invoke(facts);
} catch (NoSuchFactException e) {
log.info("Rule '{}' has been evaluated to false due to a declared but missing fact '{}' in {}",
condition.getTarget().getClass().getName(), e.getMissingFact(), facts);
return false;
} catch (IllegalArgumentException e) {
String error = "Types of injected facts in method '%s' in rule '%s' do not match parameters types";
throw new RuntimeException(format(error, condition.getMethod().getName(), condition.getTarget().getClass().getName()), e);
} catch (IllegalAccessException e) {
throw new RuntimeException("cannot access method", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("cannot invoke method", e);
}
}
@Override
public void execute(Facts facts) throws Exception {
for (InvocationDetails actionInvocation : actions) {
actionInvocation.invoke(facts);
}
}
@Override
public int compareTo(final Rule otherRule) {
int otherPriority = otherRule.getPriority();
if (priority < otherPriority) {
return -1;
} else if (priority > otherPriority) {
return 1;
} else {
String otherName = otherRule.getName();
return name.compareTo(otherName);
}
}
private static Method getConditionMethod(Object delegate) {
Method[] methods = delegate.getClass().getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Condition.class)) {
return method;
}
}
throw new RuleProcessingException("Failed to find @Condition method on " + delegate.getClass().getName());
}
private static Set<ActionMethodOrderBean> getActionMethodBeans(Object delegate) {
Method[] methods = delegate.getClass().getMethods();
Set<ActionMethodOrderBean> actionMethodBeans = new TreeSet<>();
for (Method method : methods) {
if (method.isAnnotationPresent(Action.class)) {
Action actionAnnotation = method.getAnnotation(Action.class);
int order = actionAnnotation.order();
actionMethodBeans.add(new ActionMethodOrderBean(method, order));
}
}
return actionMethodBeans;
}
@Value
private static class InvocationDetails {
Object target;
Method method;
List<String> factNames;
public InvocationDetails(Object target, Method method) {
this.target = target;
this.method = method;
this.factNames = getFactParametersNames(method);
}
public Object invoke(Facts facts) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Object[] args = new Object[factNames.size()];
for (int i = 0; i < factNames.size(); i++) {
String factName = factNames.get(i);
Object factValue = facts.get(factName);
if (factValue == null && !facts.asMap().containsKey(factName)) {
throw new NoSuchFactException(format("No fact named '%s' found in known facts: \n%s", factName, facts), factName);
}
args[i] = factValue;
}
return method.invoke(target, args);
}
private static List<String> getFactParametersNames(Method method) {
List<String> actualParameters = new ArrayList<>();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (Annotation[] annotations : parameterAnnotations) {
if (annotations.length != 1 || !(annotations[0] instanceof Fact)) {
throw new RuleProcessingException("Parameters not annotated with @Fact on " + method.getName());
}
String factName = ((Fact) (annotations[0])).value();
actualParameters.add(factName);
}
return actualParameters;
}
}
private static class NoSuchFactException extends RuntimeException {
private static final long serialVersionUID = 5629330373236051088L;
private final String missingFact;
public NoSuchFactException(String message, String missingFact) {
super(message);
this.missingFact = missingFact;
}
public String getMissingFact() {
return missingFact;
}
}
@Value
private static class ActionMethodOrderBean implements Comparable<ActionMethodOrderBean> {
private Method method;
private int order;
@Override
public int compareTo(final ActionMethodOrderBean actionMethodOrderBean) {
if (order < actionMethodOrderBean.getOrder()) {
return -1;
} else if (order > actionMethodOrderBean.getOrder()) {
return 1;
} else {
// invalid for contract of compareTo, but we copied it and might rely on some internal behavior of j-easy rules
return method.equals(actionMethodOrderBean.getMethod()) ? 0 : 1;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment