001package org.javasimon.callback;
002
003import org.javasimon.Counter;
004import org.javasimon.Simon;
005import org.javasimon.SimonException;
006import org.javasimon.SimonPattern;
007import org.javasimon.Split;
008import org.javasimon.Stopwatch;
009import org.javasimon.utils.Replacer;
010
011import java.math.BigDecimal;
012
013import javax.script.Bindings;
014import javax.script.Compilable;
015import javax.script.CompiledScript;
016import javax.script.ScriptEngine;
017import javax.script.ScriptEngineManager;
018import javax.script.ScriptException;
019
020/**
021 * Represents filtering rule that checks whether sub-callbacks will get the event.
022 * Rule can be one of the following types:
023 * <ul>
024 * <li>{@link Type#MUST} - rule MUST be true and following rules are checked
025 * <li>{@link Type#SUFFICE} - if this rule is true the filter passes the event to children
026 * otherwise next rules are checked
027 * <li>{@link Type#MUST_NOT} - if this rule is true the filter ignores the event, otherwise
028 * next rules are checked
029 * </ul>
030 * As the order is important not all MUST rules must pass if there is any satisfied SUFFICE rule before.
031 *
032 * @author <a href="mailto:virgo47@gmail.com">Richard "Virgo" Richter</a>
033 * @since 3.1 (previously was {@code FilterCallback.Rule})
034 */
035public class FilterRule {
036
037        /** Enumeration of rule types that determines the evaluation of multiple rules in a chain. */
038        public enum Type {
039                /** Rule must pass and next rule is consulted. */
040                MUST,
041
042                /**
043                 * If the rule passes the whole filter passes and no other rule is consulted. If the rule
044                 * doesn't pass next rule is consulted.
045                 */
046                SUFFICE,
047
048                /**
049                 * Rule must not pass. If the rule passes the whole filter doesn't pass. If it fails next
050                 * rule is checked.
051                 */
052                MUST_NOT
053        }
054
055        /** Name of the rule variable for last split time in ns (split). */
056        public static final String VAR_SPLIT = "split";
057
058        /** Name of the rule variable for number of concurrently active splits of a particular Simon (active). */
059        public static final String VAR_ACTIVE = "active";
060
061        /** Name of the rule variable for maximal number of concurrently active splits (maxactive). */
062        public static final String VAR_MAX_ACTIVE = "maxactive";
063
064        /** Name of the rule variable for current value of the counter (counter). */
065        public static final String VAR_COUNTER = "counter";
066
067        /** Name of the rule variable for maximal value of the Simon - stopwatch in ns, counter without unit (max). */
068        public static final String VAR_MAX = "max";
069
070        /** Name of the rule variable for minimal value of the Simon - stopwatch in ns, counter without unit (min). */
071        public static final String VAR_MIN = "min";
072
073        /** Name of the rule variable for total split time (total). */
074        public static final String VAR_TOTAL = "total";
075
076        /** Name of the rule variable for increment or decrement value (value). */
077        public static final String VAR_VALUE = "value";
078
079        private static final ScriptEngine ECMA_SCRIPT_ENGINE = new ScriptEngineManager().getEngineByName("ecmascript");
080
081        private static final Replacer[] CONDITION_REPLACERS = new Replacer[] {
082                new Replacer(" lt ", " < "),
083                new Replacer(" le ", " <= "),
084                new Replacer(" eq ", " == "),
085                new Replacer(" ne ", " != "),
086                new Replacer(" gt ", " > "),
087                new Replacer(" ge ", " >= "),
088                new Replacer(" and ", " && "),
089                new Replacer(" or ", " || "),
090                new Replacer(" not ", " ! "),
091                new Replacer("(\\d)s", "$1000000000"),
092                new Replacer("(\\d)ms", "$1000000"),
093                new Replacer("(\\d)us", "$1000"),
094        };
095
096        private Type type;
097        private String condition;
098        private CompiledScript expression;
099        private SimonPattern pattern;
100
101        /**
102         * Creates the rule with a specified type, condition and pattern. Rule can have a condition and/or a pattern.
103         * Pattern is not relevant for manager-level callback operations ({@link Callback#onManagerWarning(String, Exception)}, {@link Callback#onManagerMessage(String)}).
104         * Both condition and pattern are optional and can be null.
105         *
106         * @param type rule type determining the role of the rule in the chain of the filter
107         * @param condition additional conditional expression that must be true
108         * @param pattern Simon pattern that must match
109         */
110        public FilterRule(Type type, String condition, SimonPattern pattern) {
111                this.type = type;
112                this.condition = condition;
113                if (condition != null) {
114                        condition = condition.toLowerCase();
115                        for (Replacer conditionReplacer : CONDITION_REPLACERS) {
116                                condition = conditionReplacer.process(condition);
117                        }
118                        try {
119                                expression = ((Compilable) ECMA_SCRIPT_ENGINE).compile(condition);
120                                Bindings bindings = ECMA_SCRIPT_ENGINE.createBindings();
121                                bindings.put(VAR_ACTIVE, 0);
122                                bindings.put(VAR_COUNTER, 0);
123                                bindings.put(VAR_MAX, 0);
124                                bindings.put(VAR_MAX_ACTIVE, 0);
125                                bindings.put(VAR_MIN, 0);
126                                bindings.put(VAR_SPLIT, 0);
127                                bindings.put(VAR_TOTAL, 0);
128                                bindings.put(VAR_VALUE, 0);
129                                if (!(expression.eval(bindings) instanceof Boolean)) {
130                                        throw new SimonException("Expression '" + condition + "' does not return boolean.");
131                                }
132                        } catch (ScriptException e) {
133                                throw new SimonException(e);
134                        }
135                }
136                this.pattern = pattern;
137        }
138
139        /**
140         * Returns the type of this rule.
141         *
142         * @return type of this rule
143         */
144        public Type getType() {
145                return type;
146        }
147
148        /**
149         * Returns the additional condition of this rule. Values from the affected Simon can be checked and compared.
150         *
151         * @return additional condition of this rule
152         */
153        public String getCondition() {
154                return condition;
155        }
156
157        /**
158         * Returns the Simon pattern of this rule.
159         *
160         * @return Simon pattern of this rule
161         */
162        public SimonPattern getPattern() {
163                return pattern;
164        }
165
166        /**
167         * Checks the Simon and optional parameters against the condition specified for a rule.
168         *
169         * @param simon related Simon
170         * @param params optional parameters, e.g. value that is added to a Counter
171         * @return true if no condition is specified or the condition is satisfied, otherwise false
172         * @throws javax.script.ScriptException possible exception raised by the expression evaluation
173         */
174        public synchronized boolean checkCondition(Simon simon, Object... params) throws ScriptException {
175                if (condition == null) {
176                        return true;
177                }
178                if (simon instanceof Stopwatch) {
179                        return checkStopwtach((Stopwatch) simon, params);
180                }
181                if (simon instanceof Counter) {
182                        return checkCounter((Counter) simon, params);
183                }
184                return true;
185        }
186
187        private boolean checkCounter(Counter counter, Object... params) throws ScriptException {
188                Bindings bindings = ECMA_SCRIPT_ENGINE.createBindings();
189                processParams(bindings, params);
190                bindings.put(VAR_COUNTER, counter.getCounter());
191                bindings.put(VAR_MAX, counter.getMax());
192                bindings.put(VAR_MIN, counter.getMin());
193                return eval(bindings);
194        }
195
196        private boolean checkStopwtach(Stopwatch stopwatch, Object... params) throws ScriptException {
197                Bindings bindings = ECMA_SCRIPT_ENGINE.createBindings();
198                processParams(bindings, params);
199                bindings.put(VAR_ACTIVE, stopwatch.getActive());
200                bindings.put(VAR_COUNTER, stopwatch.getCounter());
201                bindings.put(VAR_MAX, stopwatch.getMax());
202                bindings.put(VAR_MIN, stopwatch.getMin());
203                bindings.put(VAR_MAX_ACTIVE, stopwatch.getMaxActive());
204                bindings.put(VAR_TOTAL, stopwatch.getTotal());
205                return eval(bindings);
206        }
207
208        private void processParams(Bindings bindings, Object... params) {
209                for (Object param : params) {
210                        if (param instanceof Split) {
211                                bindings.put(VAR_SPLIT, BigDecimal.valueOf(((Split) param).runningFor()));
212                        } else if (param instanceof Long) {
213                                bindings.put(VAR_VALUE, BigDecimal.valueOf((Long) param));
214                        }
215                }
216        }
217
218        private boolean eval(Bindings bindings) throws ScriptException {
219                return (Boolean) expression.eval(bindings);
220        }
221}