Last active
November 6, 2019 10:23
-
-
Save warmuuh/1fd2e6ecfabe46bca6644d78b80301cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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