package rocks.inspectit.server.diagnosis.engine.rule.factory;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import com.google.common.collect.Sets;
import rocks.inspectit.server.diagnosis.engine.rule.ActionMethod;
import rocks.inspectit.server.diagnosis.engine.rule.ConditionFailure;
import rocks.inspectit.server.diagnosis.engine.rule.ConditionMethod;
import rocks.inspectit.server.diagnosis.engine.rule.FireCondition;
import rocks.inspectit.server.diagnosis.engine.rule.RuleDefinition;
import rocks.inspectit.server.diagnosis.engine.rule.RuleDefinition.RuleDefinitionBuilder;
import rocks.inspectit.server.diagnosis.engine.rule.RuleOutput;
import rocks.inspectit.server.diagnosis.engine.rule.SessionVariableInjection;
import rocks.inspectit.server.diagnosis.engine.rule.TagInjection;
import rocks.inspectit.server.diagnosis.engine.rule.annotation.Action;
import rocks.inspectit.server.diagnosis.engine.rule.annotation.Condition;
import rocks.inspectit.server.diagnosis.engine.rule.annotation.Rule;
import rocks.inspectit.server.diagnosis.engine.rule.annotation.SessionVariable;
import rocks.inspectit.server.diagnosis.engine.rule.annotation.TagValue;
import rocks.inspectit.server.diagnosis.engine.rule.exception.RuleDefinitionException;
import rocks.inspectit.server.diagnosis.engine.tag.Tags;
import rocks.inspectit.server.diagnosis.engine.util.ReflectionUtils;
import rocks.inspectit.server.diagnosis.engine.util.ReflectionUtils.Visitor;
/**
* Utility method to work with rule classes. This class serves as static factory to create
* {@link RuleDefinition}s. So far it is not yet clear if this will stay, or a factory interface
* will be provided to enable to possibility of replaceable {@link RuleDefinition} implementations.
*
* @author Claudio Waldvogel, Alexander Wert
*/
public final class Rules {
/**
* The name of the internal initial rule to kick of processing.
*/
private static final String TRIGGER_RULE = "TRIGGER_RULE";
/**
* Private constructor.
*/
private Rules() {
}
/**
* Creates the faked output of an internal rule to start process of other rules. If we wrap the
* input to be analyzed into a RuleOutput the entire rule processing works exactly the same way
* and and the input can be access of type {@link Tags#ROOT_TAG}.
*
* @param input
* The input to be wrapped in output.
* @return RuleOutput providing the input as value of {@link Tags#ROOT_TAG}.
*/
public static RuleOutput triggerRuleOutput(Object input) {
return new RuleOutput(TRIGGER_RULE, Tags.ROOT_TAG, Collections.<ConditionFailure> emptyList(), Collections.singleton(Tags.rootTag(input)));
}
/**
* Methods transforms all given classes to {@link RuleDefinition}s.
* <p>
* Throws: RuleDefinitionException if a class transformation fails.
*
* @param classes
* The classes to be transformed.
* @return Set of {@link RuleDefinition}s.
* @throws RuleDefinitionException
* If any {@link RuleDefinition} is invalid.
*/
public static Set<RuleDefinition> define(Class<?>... classes) throws RuleDefinitionException {
return define(Arrays.asList(checkNotNull(classes, "Rule classes array must not be null!")));
}
/**
* Methods transforms all given classes to {@link RuleDefinition}s.
*
* Throws: RuleDefinitionException if a class transformation fails.
*
* @param classes
* The classes to be transformed.
* @return Set of {@link RuleDefinition}s.
* @throws RuleDefinitionException
* If any {@link RuleDefinition} is invalid.
*
*/
public static Set<RuleDefinition> define(Collection<Class<?>> classes) throws RuleDefinitionException {
checkArgument(CollectionUtils.isNotEmpty(classes), "Rule classes must not be null or empty!");
Set<RuleDefinition> ruleSet = Sets.newHashSet();
for (Class<?> clazz : classes) {
ruleSet.add(define(clazz));
}
return ruleSet;
}
/**
* Methods transforms a class to a {@link RuleDefinition}.
* <p>
* Throws: RuleDefinitionException if a class transformation fails.
*
* @param clazz
* The class to be transformed.
* @return The corresponding {@link RuleDefinition}.
* @throws RuleDefinitionException
* If {@link RuleDefinition} is invalid.
*/
public static RuleDefinition define(final Class<?> clazz) throws RuleDefinitionException {
checkNotNull(clazz, "Rule class must not be null!");
Rule annotation = ReflectionUtils.findAnnotation(clazz, Rule.class);
if (annotation == null) {
throw new RuleDefinitionException(clazz.getName() + " must be annotated with @Rule annotation.");
}
if (!ReflectionUtils.hasNoArgsConstructor(clazz)) {
throw new RuleDefinitionException(clazz.getName() + " must define an empty default constructor.");
}
ActionMethod actionMethod = describeActionMethod(clazz);
List<ConditionMethod> conditionMethods = describeConditionMethods(clazz);
List<TagInjection> tagInjections = describeTagInjection(clazz);
List<SessionVariableInjection> variableInjections = describeSessionParameterInjections(clazz);
FireCondition fireCondition = describeFireCondition(annotation, tagInjections);
RuleDefinitionBuilder builder = new RuleDefinitionBuilder();
builder.setName(annotation.name());
builder.setDescription(annotation.description());
builder.setImplementation(clazz);
builder.setFireCondition(fireCondition);
builder.setConditionMethods(conditionMethods);
builder.setActionMethod(actionMethod);
builder.setTagInjections(tagInjections);
builder.setSessionVariableInjections(variableInjections);
return builder.build();
}
// -------------------------------------------------------------
// Methods: Descriptions
// -------------------------------------------------------------
/**
* Utility method to create a {@link FireCondition} either from a {@link Rule} annotation or
* from {@link TagInjection}s. The values defined in the annotation overrules the
* {@link TagInjection}s.
*
* @param rule
* The {@link Rule} annotation.
* @param tagInjections
* The list of {@link TagInjection}s to extract a {@link FireCondition}.
* @return A new {@link FireCondition}
*/
public static FireCondition describeFireCondition(Rule rule, List<TagInjection> tagInjections) {
if ((rule != null) && ArrayUtils.isNotEmpty(rule.fireCondition())) {
return new FireCondition(Sets.newHashSet(Arrays.asList(rule.fireCondition())));
} else {
Set<String> requiredTypes = new HashSet<>();
for (TagInjection injection : tagInjections) {
requiredTypes.add(injection.getType());
}
return new FireCondition(requiredTypes);
}
}
/**
* Extracts the {@link SessionVariableInjection}s from the given class by processing the
* {@link SessionVariable} annotations.
*
* @param clazz
* The class to be analyzed.
* @return List of SessionVariableInjections
* @throws RuleDefinitionException
* If {@link SessionVariableInjection} annotations are invalid.
*/
public static List<SessionVariableInjection> describeSessionParameterInjections(Class<?> clazz) throws RuleDefinitionException {
return ReflectionUtils.visitFieldsAnnotatedWith(SessionVariable.class, clazz, new Visitor<SessionVariable, Field, SessionVariableInjection>() {
@Override
public SessionVariableInjection visit(SessionVariable annotation, Field field) {
return new SessionVariableInjection(annotation.name(), annotation.optional(), field);
}
});
}
/**
* Extracts the {@link TagInjection}s from the given class by processing the
* {@link TagInjection} annotations.
*
* @param clazz
* The class to be analyzed.
* @return List of TagInjection
* @throws RuleDefinitionException
* If {@link TagInjection} annotations are invalid. Contract is that a class must
* defined at least one field annotated with {@link TagValue}. Otherwise the rule
* could never fire, because no input could be determined.
*/
public static List<TagInjection> describeTagInjection(Class<?> clazz) throws RuleDefinitionException {
List<TagInjection> tagInjections = ReflectionUtils.visitFieldsAnnotatedWith(TagValue.class, clazz, new Visitor<TagValue, Field, TagInjection>() {
@Override
public TagInjection visit(TagValue annotation, Field field) {
return new TagInjection(annotation.type(), field, annotation.injectionStrategy());
}
});
if (CollectionUtils.isEmpty(tagInjections)) {
throw new RuleDefinitionException(clazz.getName() + " must annotate at least one field with @Value. Otherwise the " + "rule will never fire and is useless.");
} else {
return tagInjections;
}
}
/**
* Extracts the {@link Action} from the given class by processing the {@link Action} annotation.
*
* @param clazz
* The class to be analyzed.
* @return List of ActionMethod
* @throws RuleDefinitionException
* If {@link Action} annotation is invalid.
*/
public static ActionMethod describeActionMethod(Class<?> clazz) throws RuleDefinitionException {
List<ActionMethod> actionMethods = ReflectionUtils.visitMethodsAnnotatedWith(Action.class, clazz, new Visitor<Action, Method, ActionMethod>() {
@Override
public ActionMethod visit(Action annotation, Method method) throws RuleDefinitionException {
return new ActionMethod(method, annotation.resultTag(), annotation.resultQuantity());
}
});
if ((null == actionMethods) || (actionMethods.size() != 1)) {
throw new RuleDefinitionException("A rule must define exactly one method annotated with @Action. Otherwise the rule could never be exectued.");
} else {
return actionMethods.get(0);
}
}
/**
* Extracts the {@link ConditionMethod}s from the given class by processing the
* {@link Condition} annotation.
*
* @param clazz
* The class to be analyzed.
* @return List of ConditionMethods
* @throws RuleDefinitionException
* If {@link ConditionMethod} annotations are invalid.
*/
public static List<ConditionMethod> describeConditionMethods(Class<?> clazz) throws RuleDefinitionException {
List<ConditionMethod> conditions = ReflectionUtils.visitMethodsAnnotatedWith(Condition.class, clazz, new Visitor<Condition, Method, ConditionMethod>() {
@Override
public ConditionMethod visit(Condition annotation, Method method) throws RuleDefinitionException {
return new ConditionMethod(annotation.name(), annotation.hint(), method);
}
});
return conditions;
}
}