
com.crabshue.commons.aggregator.AggregatorContext Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of commons-aggregator Show documentation
Show all versions of commons-aggregator Show documentation
Library aggregating fields using annotations in complex objects structures
The newest version!
package com.crabshue.commons.aggregator;
import com.crabshue.commons.aggregator.model.AggregatorConfiguration;
import com.crabshue.commons.aggregator.model.Class;
import com.crabshue.commons.aggregator.model.ProcessTrace;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.*;
import org.apache.commons.jexl3.introspection.JexlPermissions;
import java.util.*;
import java.util.stream.Collectors;
import static com.crabshue.commons.aggregator.StringFunctions.isEmpty;
@Slf4j
public class AggregatorContext implements JexlContext.NamespaceResolver, JexlContext {
private static final int SIZE_MAX = 2000;
public static final String CONTEXT_VARIABLE = "$__context__";
private final Map analysedCache = new HashMap<>();
private final JexlEngine jexl;
private final Map registeredNamespaces;
private final JexlContext localContext;
private final Map aggregators;
private final boolean debug;
private final int sizeMax = SIZE_MAX;
private List processings;
private ProcessTrace processTrace;
private List packageStarts;
private ClassLoader classLoader = null;
private AggregatorContext(boolean debug) {
this.jexl = new JexlBuilder()
.permissions(JexlPermissions.UNRESTRICTED)
.create();
this.localContext = new MapContext();
this.localContext.set(CONTEXT_VARIABLE, this);
this.aggregators = new HashMap<>();
this.registeredNamespaces = new HashMap<>();
this.processTrace = new ProcessTrace();
this.debug = debug;
this.packageStarts = null;
}
public static Builder builder() {
return new Builder();
}
public List getProcessings() {
return processings;
}
public void setProcessings(List processing) {
this.processings = processing;
}
public ProcessTrace getProcessTrace() {
return processTrace;
}
public void setProcessTrace(ProcessTrace processTrace) {
this.processTrace = processTrace;
}
/**
* Change the default classloader for this context
*
* @param classLoader the classLoader to be used for analysis
*/
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
Map getRegisteredNamespaces() {
return registeredNamespaces;
}
Map getAnalysedCache() {
return analysedCache;
}
/**
* Computes an identifier by concatenating prefix, "." and suffix
* avoids null exception when computing ids
* if null is passed the string "null" is concatenated in place of the object
*
* @param prefix the prefix (can be null)
* @param suffix the suffix (can be null)
* @return concatenation like described
*/
public String id(Object prefix, Object suffix) {
return prefix + "." + suffix;
}
/**
* Process an object through this aggregator
*
* @param object the object to process
* @param the type of object to process deduced from Object class
* @return a processed object
*/
public T process(T object) {
Processor.process(object, object.getClass().getSimpleName(), this);
return object;
}
/**
* Register an object (or a class) to a namespace. Future call to the evaluator
* function will benefit from all public methods as functions in that namespace
*
* @param namespace the string to prefix the functions
* @param clazz the class containing functions to register
*/
public void register(String namespace, java.lang.Class clazz) {
registeredNamespaces.put(namespace, clazz);
}
public Object execute(String field, String formula) {
ProcessTrace current = processTrace;
try {
StringBuilder expression = new StringBuilder(field).append("=").append(formula);
if (debug) {
processTrace = processTrace.traceExecute(field, formula);
logger.debug("Execute:" + expression);
}
return evaluate(expression.toString());
} finally {
processTrace = current;
}
}
/**
* Evaluate the expression against the context, additionally to the JEXL syntax
* you can use - sum - avg - count - join see methods with same name in this
* class for more information
*
* @param expression expression to evaluate
* @return evaluated expression
*/
public Object evaluate(String expression) {
try {
return jexl.createExpression(expression).evaluate(this);
} catch (JexlException.Property e) {
error("Could not evaluate expression '" + expression + "' because: property '" + e.getProperty() + "' not found or of wrong type cause:'" + e.getCause() + "'", e);
return null;
} catch (JexlException e) {
JexlInfo.Detail details = e.getInfo().getDetail();
String error = details.toString();
error = error.substring(details.start(), details.end());
error("Could not evaluate expression '" + expression + "' at '" + error + "' because: '" + e.getMessage() + "'", e);
return null;
}
}
/**
* Joins all objects that have been collected in an aggregator into a string
* separated by separator
*
* @param separator the String to use as separator
* @param aggregator the aggregator to join
* @return a String with all aggregator's element joined by sperator or else an empty string if aggregator not found
*/
public Object join(String separator, String aggregator) {
return aggregate(aggregator, "join", false, a -> a.join("+'" + separator + "'+"), "");
}
/**
* Count how many objects have been collected in an aggregator
*
* @param aggregator the aggregator to count elements for
* @return the number of elements in aggregator or else 0
*/
public Integer count(String aggregator) {
if (aggregators.containsKey(aggregator)) {
int ret = aggregators.get(aggregator).count();
if (debug) {
logger.debug("count of " + aggregator + "=" + ret);
}
return ret;
} else {
warning("Could not find aggregator with name '" + aggregator + "'");
}
return 0;
}
/**
* if Object was aggregated return true.
*
* @param aggregator the aggregator to evaluate
* @param object the object to search reference for
* @return true if object exists in aggregator or else false
*/
public Boolean contains(String aggregator, Object object) {
return asSet(aggregator).contains(object);
}
/**
* Sum all objects collected in an aggregator (may be problematic if JEXL cannot
* sum those objects with "+" operand)
*
* @param aggregator the aggregator to evaluate
* @param orElse the default value to return in case not found or no elements in aggregator
* @return the total of all elements in element's type if not found or no elements
*/
public T sum(String aggregator, T orElse) {
return (T) aggregate(aggregator, "sum", true, a -> a.join("+"), orElse);
}
/**
* Sum all objects collected in an aggregator (may be problematic if JEXL cannot
* sum those objects with "+" operand)
*
* @param aggregator the aggregator
* @return the sum or null if not found or empty
*/
public Object sum(String aggregator) {
return aggregate(aggregator, "sum", true, a -> a.join("+"), null);
}
/**
* does sum/count for an aggregator, result is floating point (usually double)
*
* @param aggregator the aggregator
* @return the average or 0.0d if not found or empty
*/
public Object avg(final String aggregator) {
int count = count(aggregator);
if (count > 0) {
sum(aggregator);
return evaluate("$sum/" + count(aggregator) + ".0");
}
return 0.0d;
}
/**
* return aggregated values As Array
*
* @param aggregator the aggregator
* @return an array (may be empty)
*/
public T[] asArray(String aggregator) {
Object ret = aggregate(aggregator, "asArray", false, a -> "[" + a.join(",") + "]", new Object[]{});
if (ret instanceof int[]) {
Integer[] ret2 = new Integer[((int[]) ret).length];
int idx = 0;
for (int value : ((int[]) ret))
ret2[idx++] = value;
return (T[]) ret2;
}
if (ret instanceof double[]) {
Double[] ret2 = new Double[((double[]) ret).length];
int idx = 0;
for (double value : ((double[]) ret))
ret2[idx] = value;
return (T[]) ret2;
}
return (T[]) ret;
}
/**
* return aggregated values as Set
*
* @param aggregator the aggregator
* @return the distinct set of elements (may be empty)
*/
public Set asSet(String aggregator) {
@SuppressWarnings("unchecked")
Set ret = (Set) aggregate(aggregator, "asSet", false, a -> "{" + a.join(",") + "}", new HashSet<>());
return ret;
}
/**
* all aggregators in this context
*
* @return a list of aggregator names
*/
public Set aggregators() {
return Collections.unmodifiableSet(aggregators.keySet());
}
protected Object aggregate(String aggregator, String name, boolean canSplit,
AggregatorJEXLBuilder expressionBuilder, Object orElse) {
Object ret = null;
if (aggregators.containsKey(aggregator)) {
Aggregator a = aggregators.get(aggregator);
if (a.formulas.size() == 0)
return orElse;
if (canSplit && a.count() > sizeMax) {
boolean first = true;
for (Aggregator b : a.split(sizeMax)) {
String expression = "$" + name + (first ? "=" : "+=") + expressionBuilder.buildExpression(b);
first = false;
ret = evaluate(expression);
}
} else {
String expression = "$" + name + "=" + expressionBuilder.buildExpression(a);
ret = evaluate(expression);
}
if (debug) {
String message = name + " of '" + aggregator + "' = " + ret + (ret != null ? ("[" + ret.getClass().getSimpleName() + "]") : "");
processTrace.traceDebug(message);
logger.debug(message);
}
return ret;
} else {
warning("Could not find aggregator with name '" + aggregator + "' use value:'" + orElse + "'");
}
return orElse;
}
/**
* Used by processor to collect object references
*
* @param aggregator aggregator's name to collect
* @param objectReference the reference to the element to collect
*/
protected void collect(String aggregator, String objectReference) {
if (!aggregators.containsKey(aggregator))
aggregators.put(aggregator, new Aggregator());
aggregators.get(aggregator).append(objectReference);
if (debug) {
processTrace.traceCollect(aggregator, objectReference);
}
}
@Override
public Object resolveNamespace(String s) {
Object ret = registeredNamespaces.get(s);
return ret == null ? this : ret;
}
@Override
public Object get(String s) {
return localContext.get(s);
}
@Override
public void set(String s, Object o) {
localContext.set(s, o);
}
@Override
public boolean has(String s) {
return localContext.has(s);
}
/**
* removes all elements from context starting with prefix
*
* @param prefix the prefix of all aggregators to clean
*/
public void cleanContext(String prefix) {
for (Map.Entry entry : aggregators.entrySet()) {
if (entry.getKey().startsWith(prefix + ".")) {
entry.getValue().clear();
}
}
}
public void addVariable(String variable, Object object) {
// TODO Auto-generated method stub
localContext.set("$" + variable, object);
if (debug) {
logger.debug("got variable $" + variable);
processTrace.traceVariable(variable, object);
}
}
public String getLastProcessTrace() {
return processTrace.toString();
}
public boolean isDebug() {
return debug;
}
public void analyse(java.lang.Class clazz, Analysed analysed) {
analysed.setClassType(Analysed.CLASS_TYPE.PROCESSABLE);
analysed.addOtherFields(clazz);
analysed.prune();
analysedCache.put(clazz, analysed);
}
public List getPackageStarts() {
return packageStarts;
}
public void setPackageStarts(List packageStarts) {
this.packageStarts = packageStarts;
}
public int size() {
return 0;
}
void error(String message) {
error(message, null);
}
void error(String message, Throwable e) {
logger.error(message);
if (debug) {
processTrace.traceError(message);
if (e != null) {
String cause = getRootCause(e);
logger.error(cause);
processTrace.traceError(cause);
}
}
}
private String getRootCause(Throwable e) {
if (e.getCause() != null)
return getRootCause(e.getCause());
return e.getLocalizedMessage();
}
private void warning(String message) {
logger.warn(message);
if (debug)
processTrace.traceWarning(message);
}
public void cacheAndValidate(java.lang.Class objectClass, Analysed analysed) {
if (analysed.getExecutes() != null && analysed.getCollects() != null) {
for (String field : analysed.getExecutes().keySet()) {
if (analysed.getCollects().containsKey(field)) {
List executes = analysed.getExecutes().get(field);
List collects = analysed.getCollects().get(field);
if (!executes.isEmpty() && !collects.isEmpty()) {
Optional execute = executes.stream()
.filter(e -> e.getJexl().equals("null")).findFirst();
if (execute.isPresent()) {
if (collects.stream().anyMatch(c -> c.getWhen() == null)) {
error("Collecting nullable field '" + objectClass.getName() + "." + field
+ "' without when condition (suggestion add : when=\"not(" + execute.get().getWhen()
+ ")\"");
} else {
warning("Collecting nullable field '" + field + "' with when condition");
}
} else {
logger.info("Collecting field '" + field + "' and execute may change its value ");
}
}
}
}
}
analysedCache.put(objectClass, analysed);
}
static IgnorableClassDetector packageDetector(List analysedPackages) {
return (clazz) -> {
if (clazz.getPackage() == null) return true;
String p = clazz.getPackage().getName();
for (String pStart : analysedPackages) {
if (p.startsWith(pStart)) return false;
}
return true;
};
}
synchronized Analysed getAnalysed(java.lang.Class objectClass) {
if (!analysedCache.containsKey(objectClass)) {
Analysed analysed = new Analysed(objectClass, packageDetector(this.getPackageStarts()));
cacheAndValidate(objectClass, analysed);
}
return analysedCache.get(objectClass);
}
/**
* Load class potentially using a differentiated class loader
*
* @param name the class to load
* @return the loaded class
* @throws ClassNotFoundException when the class cannot be loaded
*/
protected java.lang.Class> loadClass(String name) throws ClassNotFoundException {
if (classLoader == null) {
return this.getClass().getClassLoader().loadClass(name);
} else {
return classLoader.loadClass(name);
}
}
void preProcess(Object o) {
if (processings != null)
processings.forEach(processing ->
processing.preProcess(o, this)
);
}
void postProcess(Object o) {
if (processings != null)
processings.forEach(processing ->
processing.postProcess(o, this)
);
}
public interface AggregatorJEXLBuilder {
String buildExpression(Aggregator a);
}
/**
* Inner structure of the aggregator, subject to change...
*/
private static class Aggregator {
List formulas;
private Aggregator() {
this.formulas = new ArrayList<>();
}
public List split(int size) {
List ret = new ArrayList<>();
for (int start = 0; start < formulas.size(); start += size) {
int end = Math.min(start + size, formulas.size());
Aggregator b = new Aggregator();
b.formulas = formulas.subList(start, end);
ret.add(b);
}
return ret;
}
private void append(String formula) {
this.formulas.add(formula);
}
private int count() {
return formulas.size();
}
private String join(String s) {
return String.join(s, formulas);
}
public void clear() {
formulas.clear();
}
}
public static class Builder {
boolean debug;
ClassLoader classLoader;
AggregatorConfiguration config;
private Builder() {
}
private static void analyseProcessing(AggregatorContext context, List processings) {
if (processings == null)
return;
context.setProcessings(
processings.stream()
.map(processing -> {
try {
Object o = context.loadClass(processing).newInstance();
if (o instanceof AggregatorProcessing) {
return (AggregatorProcessing) o;
} else {
context.error(processing + " do not implement " + AggregatorProcessing.class.getName());
}
} catch (Throwable e) {
context.error("Cannot register processing class " + processing, e);
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList()));
}
private static void analyseFunction(AggregatorContext context, AggregatorConfiguration config) {
config.getFunctionList().stream()
.filter(f -> !isEmpty(f.getRegisterClass()) && !isEmpty(f.getNamespace()))
.forEach(function -> {
java.lang.Class clazz;
try {
clazz = context.loadClass(function.getRegisterClass());
context.register(function.getNamespace(), clazz);
} catch (ClassNotFoundException e) {
context.error("Cannot register namespace function" + function.getRegisterClass(), e);
}
});
}
private static void analysePackages(AggregatorContext context, List packagesStart) {
if (packagesStart != null)
context.setPackageStarts(packagesStart);
}
private static void analyseClass(AggregatorContext context, AggregatorConfiguration config) {
config.getClassList().stream()
.filter(c -> !isEmpty(c.getClassName()))
.forEach(c -> analyseAClass(context, c));
}
private static void analyseAClass(AggregatorContext context, Class clazzConfig) {
java.lang.Class clazz;
try {
clazz = context.loadClass(clazzConfig.getClassName());
} catch (ClassNotFoundException e) {
context.error("Cannot analyse class:" + clazzConfig.getClassName(), e);
return;
}
Analysed analysed = new Analysed();
analysed.setClassContext(clazzConfig.getClassContext());
analyseClassConfig(clazzConfig, analysed);
analysed.setClassType(Analysed.isProcessable(clazz) ? Analysed.CLASS_TYPE.PROCESSABLE : Analysed.CLASS_TYPE.IGNORABLE);
analysed.addOtherFields(clazz);
analysed.prune();
context.cacheAndValidate(clazz, analysed);
}
private static void analyseClassConfig(Class clazzConfig, Analysed analysed) {
clazzConfig.getExecuteList()
.forEach(e -> analysed.addExecute(e.getField(), e.getJexl(), e.getWhen()));
clazzConfig.getCollectList()
.stream()
.filter(collect -> !isEmpty(collect.getField()))
.forEach(c -> analysed.addCollectField(c.getField(), c.getTo(), c.getWhen()));
clazzConfig.getCollectList()
.stream()
.filter(collect -> !isEmpty(collect.getWhat()))
.forEach(c -> analysed.addCollectClass(c.getWhat(), c.getTo(), c.getWhen()));
clazzConfig.getVariableList()
.forEach(variable -> analysed.addVariable(variable.getField(), variable.getVariable()));
}
/**
* setup the configuration to be used;
*
* @param config configuration model
* @return the builder
*/
public Builder config(AggregatorConfiguration config) {
this.config = config;
return this;
}
/**
* setup the debug parameter
*
* @param debug enables debug mode
* @return the builder
*/
public Builder debug(boolean debug) {
this.debug = debug;
return this;
}
/**
* sets the class loader to use
*
* @param classLoader a class loader
* @return the builder
*/
public Builder classLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
return this;
}
/**
* Builds a new aggregator context
*
* @return AggregatorContext
*/
public AggregatorContext build() {
AggregatorContext context = new AggregatorContext(debug);
if (classLoader != null)
context.setClassLoader(classLoader);
if (config != null) {
analyseProcessing(context, config.getProcessings());
analysePackages(context, config.getAnalysedPackages());
analyseFunction(context, config);
analyseClass(context, config);
}
return context;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy