001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import java.lang.reflect.InvocationTargetException;
005import java.lang.reflect.Method;
006import java.text.MessageFormat;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.EnumSet;
010import java.util.Map;
011import java.util.Objects;
012import java.util.Set;
013import java.util.regex.Pattern;
014
015import org.openstreetmap.josm.Main;
016import org.openstreetmap.josm.actions.search.SearchCompiler.InDataSourceArea;
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.data.osm.OsmUtils;
020import org.openstreetmap.josm.data.osm.Relation;
021import org.openstreetmap.josm.data.osm.Tag;
022import org.openstreetmap.josm.data.osm.Way;
023import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
024import org.openstreetmap.josm.gui.mappaint.Cascade;
025import org.openstreetmap.josm.gui.mappaint.ElemStyles;
026import org.openstreetmap.josm.gui.mappaint.Environment;
027import org.openstreetmap.josm.tools.CheckParameterUtil;
028import org.openstreetmap.josm.tools.Predicate;
029import org.openstreetmap.josm.tools.Predicates;
030import org.openstreetmap.josm.tools.Utils;
031
032public abstract class Condition {
033
034    public abstract boolean applies(Environment e);
035
036    public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) {
037        switch (context) {
038        case PRIMITIVE:
039            if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey)
040                return new KeyValueRegexpCondition(k, v, op, false);
041            if (!considerValAsKey && op.equals(Op.EQ))
042                return new SimpleKeyValueCondition(k, v);
043            return new KeyValueCondition(k, v, op, considerValAsKey);
044        case LINK:
045            if (considerValAsKey)
046                throw new MapCSSException("''considerValAsKey'' not supported in LINK context");
047            if ("role".equalsIgnoreCase(k))
048                return new RoleCondition(v, op);
049            else if ("index".equalsIgnoreCase(k))
050                return new IndexCondition(v, op);
051            else
052                throw new MapCSSException(
053                        MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
054
055        default: throw new AssertionError();
056        }
057    }
058
059    public static Condition createRegexpKeyRegexpValueCondition(String k, String v, Op op) {
060        return new RegexpKeyValueRegexpCondition(k, v, op);
061    }
062
063    public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) {
064        switch (context) {
065        case PRIMITIVE:
066            return new KeyCondition(k, not, matchType);
067        case LINK:
068            if (matchType != null)
069                throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context");
070            if (not)
071                return new RoleCondition(k, Op.NEQ);
072            else
073                return new RoleCondition(k, Op.EQ);
074
075        default: throw new AssertionError();
076        }
077    }
078
079    public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
080        return PseudoClassCondition.createPseudoClassCondition(id, not, context);
081    }
082
083    public static ClassCondition createClassCondition(String id, boolean not, Context context) {
084        return new ClassCondition(id, not);
085    }
086
087    public static ExpressionCondition createExpressionCondition(Expression e, Context context) {
088        return new ExpressionCondition(e);
089    }
090
091    /**
092     * This is the operation that {@link KeyValueCondition} uses to match.
093     */
094    public enum Op {
095        /** The value equals the given reference. */
096        EQ,
097        /** The value does not equal the reference. */
098        NEQ,
099        /** The value is greater than or equal to the given reference value (as float). */
100        GREATER_OR_EQUAL,
101        /** The value is greater than the given reference value (as float). */
102        GREATER,
103        /** The value is less than or equal to the given reference value (as float). */
104        LESS_OR_EQUAL,
105        /** The value is less than the given reference value (as float). */
106        LESS,
107        /** The reference is treated as regular expression and the value needs to match it. */
108        REGEX,
109        /** The reference is treated as regular expression and the value needs to not match it. */
110        NREGEX,
111        /** The reference is treated as a list separated by ';'. Spaces around the ; are ignored.
112         *  The value needs to be equal one of the list elements. */
113        ONE_OF,
114        /** The value needs to begin with the reference string. */
115        BEGINS_WITH,
116        /** The value needs to end with the reference string. */
117        ENDS_WITH,
118        /** The value needs to contain the reference string. */
119        CONTAINS;
120
121        static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX);
122
123        /**
124         * Evaluates a value against a reference string.
125         * @param testString The value. May be <code>null</code>
126         * @param prototypeString The reference string-
127         * @return <code>true</code> if and only if this operation matches for the given value/reference pair.
128         */
129        public boolean eval(String testString, String prototypeString) {
130            if (testString == null && !NEGATED_OPS.contains(this))
131                return false;
132            switch (this) {
133            case EQ:
134                return Objects.equals(testString, prototypeString);
135            case NEQ:
136                return !Objects.equals(testString, prototypeString);
137            case REGEX:
138            case NREGEX:
139                final boolean contains = Pattern.compile(prototypeString).matcher(testString).find();
140                return REGEX.equals(this) ? contains : !contains;
141            case ONE_OF:
142                return testString != null && Arrays.asList(testString.split("\\s*;\\s*")).contains(prototypeString);
143            case BEGINS_WITH:
144                return testString != null && testString.startsWith(prototypeString);
145            case ENDS_WITH:
146                return testString != null && testString.endsWith(prototypeString);
147            case CONTAINS:
148                return testString != null && testString.contains(prototypeString);
149            }
150
151            float testFloat;
152            try {
153                testFloat = Float.parseFloat(testString);
154            } catch (NumberFormatException e) {
155                return false;
156            }
157            float prototypeFloat = Float.parseFloat(prototypeString);
158
159            switch (this) {
160            case GREATER_OR_EQUAL:
161                return testFloat >= prototypeFloat;
162            case GREATER:
163                return testFloat > prototypeFloat;
164            case LESS_OR_EQUAL:
165                return testFloat <= prototypeFloat;
166            case LESS:
167                return testFloat < prototypeFloat;
168            default:
169                throw new AssertionError();
170            }
171        }
172    }
173
174    /**
175     * Context, where the condition applies.
176     */
177    public enum Context {
178        /**
179         * normal primitive selector, e.g. way[highway=residential]
180         */
181        PRIMITIVE,
182
183        /**
184         * link between primitives, e.g. relation &gt;[role=outer] way
185         */
186        LINK
187    }
188
189    /**
190     * Most common case of a KeyValueCondition, this is the basic key=value case.
191     *
192     * Extra class for performance reasons.
193     */
194    public static class SimpleKeyValueCondition extends Condition {
195        /**
196         * The key to search for.
197         */
198        public final String k;
199        /**
200         * The value to search for.
201         */
202        public final String v;
203
204        /**
205         * Create a new SimpleKeyValueCondition.
206         * @param k The key
207         * @param v The value.
208         */
209        public SimpleKeyValueCondition(String k, String v) {
210            this.k = k;
211            this.v = v;
212        }
213
214        @Override
215        public boolean applies(Environment e) {
216            return v.equals(e.osm.get(k));
217        }
218
219        public Tag asTag() {
220            return new Tag(k, v);
221        }
222
223        @Override
224        public String toString() {
225            return '[' + k + '=' + v + ']';
226        }
227
228    }
229
230    /**
231     * <p>Represents a key/value condition which is either applied to a primitive.</p>
232     *
233     */
234    public static class KeyValueCondition extends Condition {
235        /**
236         * The key to search for.
237         */
238        public final String k;
239        /**
240         * The value to search for.
241         */
242        public final String v;
243        /**
244         * The key/value match operation.
245         */
246        public final Op op;
247        /**
248         * If this flag is set, {@link #v} is treated as a key and the value is the value set for that key.
249         */
250        public boolean considerValAsKey;
251
252        /**
253         * <p>Creates a key/value-condition.</p>
254         *
255         * @param k the key
256         * @param v the value
257         * @param op the operation
258         * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
259         */
260        public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) {
261            this.k = k;
262            this.v = v;
263            this.op = op;
264            this.considerValAsKey = considerValAsKey;
265        }
266
267        @Override
268        public boolean applies(Environment env) {
269            return op.eval(env.osm.get(k), considerValAsKey ? env.osm.get(v) : v);
270        }
271
272        public Tag asTag() {
273            return new Tag(k, v);
274        }
275
276        @Override
277        public String toString() {
278            return '[' + k + '\'' + op + '\'' + v + ']';
279        }
280    }
281
282    public static class KeyValueRegexpCondition extends KeyValueCondition {
283
284        public final Pattern pattern;
285        protected static final Set<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX);
286
287        public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) {
288            super(k, v, op, considerValAsKey);
289            CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported");
290            CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX");
291            this.pattern = Pattern.compile(v);
292        }
293
294        protected boolean matches(Environment env) {
295            final String value = env.osm.get(k);
296            return value != null && pattern.matcher(value).find();
297        }
298
299        @Override
300        public boolean applies(Environment env) {
301            if (Op.REGEX.equals(op)) {
302                return matches(env);
303            } else if (Op.NREGEX.equals(op)) {
304                return !matches(env);
305            } else {
306                throw new IllegalStateException();
307            }
308        }
309    }
310
311    public static class RegexpKeyValueRegexpCondition extends KeyValueRegexpCondition {
312
313        public final Pattern keyPattern;
314
315        public RegexpKeyValueRegexpCondition(String k, String v, Op op) {
316            super(k, v, op, false);
317            this.keyPattern = Pattern.compile(k);
318        }
319
320        @Override
321        protected boolean matches(Environment env) {
322            for (Map.Entry<String, String> kv: env.osm.getKeys().entrySet()) {
323                if (keyPattern.matcher(kv.getKey()).find() && pattern.matcher(kv.getValue()).find()) {
324                    return true;
325                }
326            }
327            return false;
328        }
329    }
330
331    public static class RoleCondition extends Condition {
332        public final String role;
333        public final Op op;
334
335        public RoleCondition(String role, Op op) {
336            this.role = role;
337            this.op = op;
338        }
339
340        @Override
341        public boolean applies(Environment env) {
342            String testRole = env.getRole();
343            if (testRole == null) return false;
344            return op.eval(testRole, role);
345        }
346    }
347
348    public static class IndexCondition extends Condition {
349        public final String index;
350        public final Op op;
351
352        public IndexCondition(String index, Op op) {
353            this.index = index;
354            this.op = op;
355        }
356
357        @Override
358        public boolean applies(Environment env) {
359            if (env.index == null) return false;
360            if (index.startsWith("-")) {
361                return env.count != null && op.eval(Integer.toString(env.index - env.count), index);
362            } else {
363                return op.eval(Integer.toString(env.index + 1), index);
364            }
365        }
366    }
367
368    /**
369     * This defines how {@link KeyCondition} matches a given key.
370     */
371    public enum KeyMatchType {
372        /**
373         * The key needs to be equal to the given label.
374         */
375        EQ,
376        /**
377         * The key needs to have a true value (yes, ...)
378         * @see OsmUtils#isTrue(String)
379         */
380        TRUE,
381        /**
382         * The key needs to have a false value (no, ...)
383         * @see OsmUtils#isFalse(String)
384         */
385        FALSE,
386        /**
387         * The key needs to match the given regular expression.
388         */
389        REGEX
390    }
391
392    /**
393     * <p>KeyCondition represent one of the following conditions in either the link or the
394     * primitive context:</p>
395     * <pre>
396     *     ["a label"]  PRIMITIVE:   the primitive has a tag "a label"
397     *                  LINK:        the parent is a relation and it has at least one member with the role
398     *                               "a label" referring to the child
399     *
400     *     [!"a label"]  PRIMITIVE:  the primitive doesn't have a tag "a label"
401     *                   LINK:       the parent is a relation but doesn't have a member with the role
402     *                               "a label" referring to the child
403     *
404     *     ["a label"?]  PRIMITIVE:  the primitive has a tag "a label" whose value evaluates to a true-value
405     *                   LINK:       not supported
406     *
407     *     ["a label"?!] PRIMITIVE:  the primitive has a tag "a label" whose value evaluates to a false-value
408     *                   LINK:       not supported
409     * </pre>
410     */
411    public static class KeyCondition extends Condition {
412
413        /**
414         * The key name.
415         */
416        public final String label;
417        /**
418         * If we should negate the result of the match.
419         */
420        public final boolean negateResult;
421        /**
422         * Describes how to match the label against the key.
423         * @see KeyMatchType
424         */
425        public final KeyMatchType matchType;
426        /**
427         * A predicate used to match a the regexp against the key. Only used if the match type is regexp.
428         */
429        public final Predicate<String> containsPattern;
430
431        /**
432         * Creates a new KeyCondition
433         * @param label The key name (or regexp) to use.
434         * @param negateResult If we should negate the result.,
435         * @param matchType The match type.
436         */
437        public KeyCondition(String label, boolean negateResult, KeyMatchType matchType) {
438            this.label = label;
439            this.negateResult = negateResult;
440            this.matchType = matchType == null ? KeyMatchType.EQ : matchType;
441            this.containsPattern = KeyMatchType.REGEX.equals(matchType)
442                    ? Predicates.stringContainsPattern(Pattern.compile(label))
443                    : null;
444        }
445
446        @Override
447        public boolean applies(Environment e) {
448            switch(e.getContext()) {
449            case PRIMITIVE:
450                switch (matchType) {
451                case TRUE:
452                    return e.osm.isKeyTrue(label) ^ negateResult;
453                case FALSE:
454                    return e.osm.isKeyFalse(label) ^ negateResult;
455                case REGEX:
456                    return Utils.exists(e.osm.keySet(), containsPattern) ^ negateResult;
457                default:
458                    return e.osm.hasKey(label) ^ negateResult;
459                }
460            case LINK:
461                Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context");
462                return false;
463            default: throw new AssertionError();
464            }
465        }
466
467        /**
468         * Get the matched key and the corresponding value.
469         * <p>
470         * WARNING: This ignores {@link #negateResult}.
471         * <p>
472         * WARNING: For regexp, the regular expression is returned instead of a key if the match failed.
473         * @param p The primitive to get the value from.
474         * @return The tag.
475         */
476        public Tag asTag(OsmPrimitive p) {
477            String key = label;
478            if (KeyMatchType.REGEX.equals(matchType)) {
479                final Collection<String> matchingKeys = Utils.filter(p.keySet(), containsPattern);
480                if (!matchingKeys.isEmpty()) {
481                    key = matchingKeys.iterator().next();
482                }
483            }
484            return new Tag(key, p.get(key));
485        }
486
487        @Override
488        public String toString() {
489            return '[' + (negateResult ? "!" : "") + label + ']';
490        }
491    }
492
493    public static class ClassCondition extends Condition {
494
495        public final String id;
496        public final boolean not;
497
498        public ClassCondition(String id, boolean not) {
499            this.id = id;
500            this.not = not;
501        }
502
503        @Override
504        public boolean applies(Environment env) {
505            return env != null && env.getCascade(env.layer) != null && not ^ env.getCascade(env.layer).containsKey(id);
506        }
507
508        @Override
509        public String toString() {
510            return (not ? "!" : "") + '.' + id;
511        }
512    }
513
514    /**
515     * Like <a href="http://www.w3.org/TR/css3-selectors/#pseudo-classes">CSS pseudo classes</a>, MapCSS pseudo classes
516     * are written in lower case with dashes between words.
517     */
518    static class PseudoClasses {
519
520        /**
521         * {@code closed} tests whether the way is closed or the relation is a closed multipolygon
522         * @param e MapCSS environment
523         * @return {@code true} if the way is closed or the relation is a closed multipolygon
524         */
525        static boolean closed(Environment e) { // NO_UCD (unused code)
526            if (e.osm instanceof Way && ((Way) e.osm).isClosed())
527                return true;
528            if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon())
529                return true;
530            return false;
531        }
532
533        /**
534         * {@code :modified} tests whether the object has been modified.
535         * @param e MapCSS environment
536         * @return {@code true} if the object has been modified
537         * @see OsmPrimitive#isModified()
538         */
539        static boolean modified(Environment e) { // NO_UCD (unused code)
540            return e.osm.isModified() || e.osm.isNewOrUndeleted();
541        }
542
543        /**
544         * {@code ;new} tests whether the object is new.
545         * @param e MapCSS environment
546         * @return {@code true} if the object is new
547         * @see OsmPrimitive#isNew()
548         */
549        static boolean _new(Environment e) { // NO_UCD (unused code)
550            return e.osm.isNew();
551        }
552
553        /**
554         * {@code :connection} tests whether the object is a connection node.
555         * @param e MapCSS environment
556         * @return {@code true} if the object is a connection node
557         * @see Node#isConnectionNode()
558         */
559        static boolean connection(Environment e) { // NO_UCD (unused code)
560            return e.osm instanceof Node && e.osm.getDataSet() != null && ((Node) e.osm).isConnectionNode();
561        }
562
563        /**
564         * {@code :tagged} tests whether the object is tagged.
565         * @param e MapCSS environment
566         * @return {@code true} if the object is tagged
567         * @see OsmPrimitive#isTagged()
568         */
569        static boolean tagged(Environment e) { // NO_UCD (unused code)
570            return e.osm.isTagged();
571        }
572
573        /**
574         * {@code :same-tags} tests whether the object has the same tags as its child/parent.
575         * @param e MapCSS environment
576         * @return {@code true} if the object has the same tags as its child/parent
577         * @see OsmPrimitive#hasSameInterestingTags(OsmPrimitive)
578         */
579        static boolean sameTags(Environment e) { // NO_UCD (unused code)
580            return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent));
581        }
582
583        /**
584         * {@code :area-style} tests whether the object has an area style. This is useful for validators.
585         * @param e MapCSS environment
586         * @return {@code true} if the object has an area style
587         * @see ElemStyles#hasAreaElemStyle(OsmPrimitive, boolean)
588         */
589        static boolean areaStyle(Environment e) { // NO_UCD (unused code)
590            // only for validator
591            return ElemStyles.hasAreaElemStyle(e.osm, false);
592        }
593
594        /**
595         * {@code unconnected}: tests whether the object is a unconnected node.
596         * @param e MapCSS environment
597         * @return {@code true} if the object is a unconnected node
598         */
599        static boolean unconnected(Environment e) { // NO_UCD (unused code)
600            return e.osm instanceof Node && OsmPrimitive.getFilteredList(e.osm.getReferrers(), Way.class).isEmpty();
601        }
602
603        /**
604         * {@code righthandtraffic} checks if there is right-hand traffic at the current location.
605         * @param e MapCSS environment
606         * @return {@code true} if there is right-hand traffic at the current location
607         * @see ExpressionFactory.Functions#is_right_hand_traffic(Environment)
608         */
609        static boolean righthandtraffic(Environment e) { // NO_UCD (unused code)
610            return ExpressionFactory.Functions.is_right_hand_traffic(e);
611        }
612
613        /**
614         * {@code clockwise} whether the way is closed and oriented clockwise,
615         * or non-closed and the 1st, 2nd and last node are in clockwise order.
616         * @param e MapCSS environment
617         * @return {@code true} if the way clockwise
618         * @see ExpressionFactory.Functions#is_clockwise(Environment)
619         */
620        static boolean clockwise(Environment e) { // NO_UCD (unused code)
621            return ExpressionFactory.Functions.is_clockwise(e);
622        }
623
624        /**
625         * {@code anticlockwise} whether the way is closed and oriented anticlockwise,
626         * or non-closed and the 1st, 2nd and last node are in anticlockwise order.
627         * @param e MapCSS environment
628         * @return {@code true} if the way clockwise
629         * @see ExpressionFactory.Functions#is_anticlockwise(Environment)
630         */
631        static boolean anticlockwise(Environment e) { // NO_UCD (unused code)
632            return ExpressionFactory.Functions.is_anticlockwise(e);
633        }
634
635        /**
636         * {@code unclosed-multipolygon} tests whether the object is an unclosed multipolygon.
637         * @param e MapCSS environment
638         * @return {@code true} if the object is an unclosed multipolygon
639         */
640        static boolean unclosed_multipolygon(Environment e) { // NO_UCD (unused code)
641            return e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon() &&
642                    !e.osm.isIncomplete() && !((Relation) e.osm).hasIncompleteMembers() &&
643                    !MultipolygonCache.getInstance().get(Main.map.mapView, (Relation) e.osm).getOpenEnds().isEmpty();
644        }
645
646        private static final Predicate<OsmPrimitive> IN_DOWNLOADED_AREA = new InDataSourceArea(false);
647
648        /**
649         * {@code in-downloaded-area} tests whether the object is within source area ("downloaded area").
650         * @param e MapCSS environment
651         * @return {@code true} if the object is within source area ("downloaded area")
652         * @see InDataSourceArea
653         */
654        static boolean inDownloadedArea(Environment e) { // NO_UCD (unused code)
655            return IN_DOWNLOADED_AREA.evaluate(e.osm);
656        }
657
658        static boolean completely_downloaded(Environment e) { // NO_UCD (unused code)
659            if (e.osm instanceof Relation) {
660                return !((Relation) e.osm).hasIncompleteMembers();
661            } else {
662                return true;
663            }
664        }
665
666        static boolean closed2(Environment e) { // NO_UCD (unused code)
667            if (e.osm instanceof Way && ((Way) e.osm).isClosed())
668                return true;
669            if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon())
670                return MultipolygonCache.getInstance().get(Main.map.mapView, (Relation) e.osm).getOpenEnds().isEmpty();
671            return false;
672        }
673
674        static boolean selected(Environment e) { // NO_UCD (unused code)
675            Cascade c = e.mc.getCascade(e.layer);
676            c.setDefaultSelectedHandling(false);
677            return e.osm.isSelected();
678        }
679    }
680
681    public static class PseudoClassCondition extends Condition {
682
683        public final Method method;
684        public final boolean not;
685
686        protected PseudoClassCondition(Method method, boolean not) {
687            this.method = method;
688            this.not = not;
689        }
690
691        public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
692            CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK.equals(context), "sameTags only supported in LINK context");
693            if ("open_end".equals(id)) {
694                return new OpenEndPseudoClassCondition(not);
695            }
696            final Method method = getMethod(id);
697            if (method != null) {
698                return new PseudoClassCondition(method, not);
699            }
700            throw new MapCSSException("Invalid pseudo class specified: " + id);
701        }
702
703        protected static Method getMethod(String id) {
704            id = id.replaceAll("-|_", "");
705            for (Method method : PseudoClasses.class.getDeclaredMethods()) {
706                // for backwards compatibility, consider :sameTags == :same-tags == :same_tags (#11150)
707                final String methodName = method.getName().replaceAll("-|_", "");
708                if (methodName.equalsIgnoreCase(id)) {
709                    return method;
710                }
711            }
712            return null;
713        }
714
715        @Override
716        public boolean applies(Environment e) {
717            try {
718                return not ^ (Boolean) method.invoke(null, e);
719            } catch (IllegalAccessException | InvocationTargetException ex) {
720                throw new RuntimeException(ex);
721            }
722        }
723
724        @Override
725        public String toString() {
726            return (not ? "!" : "") + ':' + method.getName();
727        }
728    }
729
730    public static class OpenEndPseudoClassCondition extends PseudoClassCondition {
731        public OpenEndPseudoClassCondition(boolean not) {
732            super(null, not);
733        }
734
735        @Override
736        public boolean applies(Environment e) {
737            return true;
738        }
739    }
740
741    public static class ExpressionCondition extends Condition {
742
743        private final Expression e;
744
745        /**
746         * Constructs a new {@code ExpressionFactory}
747         * @param e expression
748         */
749        public ExpressionCondition(Expression e) {
750            this.e = e;
751        }
752
753        @Override
754        public boolean applies(Environment env) {
755            Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
756            return b != null && b;
757        }
758
759        @Override
760        public String toString() {
761            return '[' + e.toString() + ']';
762        }
763    }
764}