Class MvelExpressionLanguage

java.lang.Object
io.github.brantunger.unruly.mvel.MvelExpressionLanguage
All Implemented Interfaces:
ExpressionLanguage

public final class MvelExpressionLanguage extends Object implements ExpressionLanguage
MVEL 2 as an expression language for the engine, named "mvel". A rule whose language is null is written in MVEL when MVEL is the engine's only language, or when RulesEngineBuilder.defaultLanguage(String) names it.
  • Conditions and actions have the same access to the JVM as Java code, including processes, files and reflection. There is no sandbox, so only use rules from trusted sources.
  • A condition is rejected when its text contains an assignment, ++, --, or the keywords with, def, function or import_static. A write made by calling a method, such as claim.setApproved(true), can't be detected.
  • Variables an action declares stay local to that action, and assigning to output fails.
  • A fact name must be a Java identifier that isn't one of MVEL's reserved words, such as empty or in, and isn't a class name MVEL resolves instead, such as Math or an imported class, nor the first part of the name of a class the rule list's expressions name with its package, such as java for java.lang.Integer.MAX_VALUE, or find through an import, nor the name of a class with no package the rule list's expressions use, or of the class one they use is nested in. MVEL checks a fact's name only where the text of the rule list's MVEL expressions holds it: as a name, or as a span of a word, a run of text between MVEL's whitespace (U+0000 to U+0020), that starts at the word's start, just after a character that isn't part of an identifier, or just after isdef glued to the name that follows it, and stops at the word's end or just before a character that isn't part of an identifier, such as my-fact in my-fact == 1 or my-fact-1, which MVEL reads as my minus fact, \a in 1-\a, my] in x^my]+=1, #a in isdef#a or ,a in ,a#b; or as a backslash or a high surrogate alone. That is a best effort: a name MVEL reads with whitespace in it, such as one isdef reads up to a comment on a later line, isn't checked. MVEL checks every fact for a rule list with a word of more than 1,000 characters, or with more than 64 characters that aren't part of an identifier, each one just after isdef counted twice.
  • MVEL caches accessors in a compiled expression without synchronization, so each session, which one run uses at a time, runs its own compiled copy of each expression.
  • MVEL's imports are Java packages and classes, given with RulesEngineBuilder.imports(String...). It takes no imports of its own: given any with RulesEngineBuilder.languageImports(String, String...), newCompiler(CompileContext) throws IllegalArgumentException. The engine creates MVEL's compiler when a rule list given to load() or validate() has an MVEL rule, and for a list with no rules too when MVEL is the default language. That rule list then fails: load() throws, and validate() returns, a RuleCompilationException with the throw as its cause. A rule list that doesn't use MVEL never has its imports checked.
See Also:
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final String
    The language's name.
  • Constructor Summary

    Constructors
    Constructor
    Description
    Creates the MVEL language.
  • Method Summary

    Modifier and Type
    Method
    Description
    Returns the language's name, such as "mvel".
    Creates the compiler for one rule list, with the context's Java imports, class loader, declared facts and options.
    void
    Initializes the classes with a static initializer that loading MVEL rules, running them and a load that fails would otherwise be the first to use: this language's own, MVEL's, and those of the JDK that only they use, such as the JDK's logging, which MVEL logs with, and BigInteger and BigDecimal, which MVEL makes a rule's first BigDecimal literal, such as 1.5B, with.
    Returns "output", the name MVEL's actions see the output object by, which would hide a fact of the same name.
    boolean
    Returns false: only MVEL's actions see the output object by "output", so the name is reserved only for a rule list with an MVEL rule, or with no rules while MVEL is the default language.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

  • Constructor Details

    • MvelExpressionLanguage

      public MvelExpressionLanguage()
      Creates the MVEL language. It holds no state: each rule list's state lives in its compiler.
  • Method Details

    • name

      public String name()
      Description copied from interface: ExpressionLanguage
      Returns the language's name, such as "mvel". It must return the same name every time. The engine reads it once, when the language is given to the builder or found with ServiceLoader, and knows the language by that name from then on.
      Specified by:
      name in interface ExpressionLanguage
      Returns:
      The name, never null or blank
    • newCompiler

      public ExpressionCompiler newCompiler(CompileContext context)
      Creates the compiler for one rule list, with the context's Java imports, class loader, declared facts and options.
      Specified by:
      newCompiler in interface ExpressionLanguage
      Parameters:
      context - The imports and class loader the rule list is compiled with
      Returns:
      A new compiler, used for this rule list only
      Throws:
      IllegalArgumentException - if the context has language imports, which MVEL doesn't take, or an option MVEL can't apply
    • prepare

      public void prepare()
      Initializes the classes with a static initializer that loading MVEL rules, running them and a load that fails would otherwise be the first to use: this language's own, MVEL's, and those of the JDK that only they use, such as the JDK's logging, which MVEL logs with, and BigInteger and BigDecimal, which MVEL makes a rule's first BigDecimal literal, such as 1.5B, with. On JDK 25 and later, BigDecimal's static initializer initializes the JDK's ForkJoinPool and ForkJoinTask too, without starting a thread, so a setting of the common pool made after this has no effect (#1115). So are this module's and MVEL's classes without one that a first load and a first run would otherwise be the first to read from a file: those that create the compiler and compile a rule, the session and an action's variables (#1210). The JDK's classes that MVEL's first read of a method's generic return type and its first reflective call of a method with a runtime annotation initialize are the engine's to initialize, as it builds the JVM's first engine. A load or a run may be nested deep in another run's stack, where a StackOverflowError inside one of them would leave the class unusable for the life of the JVM, and so every later MVEL load or run, or every later report of a compile error. The engine calls this once it has made room for it: when it builds an engine whose builder names MVEL, as its default language or otherwise, or else when a rule list first uses MVEL. They're initialized in the order a first load and run initialize them, so MVEL's optimizer sets up as MvelExpression sets it up, and only those that this module's code uses directly: each initializes what it needs itself, which depends on MVEL's settings, such as whether its JIT is on; and MVEL's GetterAccessor, which a rule's first read of a property through its getter initializes. Its static initializer creates the empty array that MVEL's calls of a method or a constructor with no arguments read, which catch no Error, so one left unusable would fail every such call and every new without arguments, in every MVEL engine for the life of the JVM. Not those of MVEL's other features: a rule's first inline list or map, new or soundslike initializes MVEL classes of its own, which this doesn't, so that first use, like the JIT's first compile, can overflow in one of them deep in a stack, which leaves only that feature unusable. Nor those of MVEL's JIT, which the engine replaces as MVEL's default with its reflective optimizer, but which an application may make the default again: the JIT compiles an accessor with ASM once more than 50 runs have used it within 100 ms, and its first compile initializes ASM's classes and the JDK's that it uses. MVEL marks an accessor compiled before it compiles it, so a compile that fails, such as one that overflows deep in a stack, fails that run and isn't tried again: the accessor stays reflective, still correct. If the overflow strikes a class's static initializer, of ASM's or the JDK's, that class stays unusable for the JVM's life, and every other accessor's first compile fails one run the same way. Then it evaluates a property read, a method call, a method call with a literal argument and a comparison with null once each, through MVEL's public API, on a value of this module's own, and an assignment, and compiles a call of a method the value doesn't have. A JVM's first read, first call and first literal argument load classes MVEL evaluates them with, which takes more stack than run()'s own check of the room makes sure of, so a first run deep in a stack can fail its rule; loading them here leaves that run fewer to load, so it needs less stack. Not nothing: a rule's first call of a method makes the JDK build the method's reflective accessor, which for a signature of a shape no call has had yet generates classes, and deep in a stack that can still overflow and fail the rule. Until a run returns normally having run an action of an MVEL rule, the engine checks runs for more room, which covered a first call of one method in the shapes measured, one with a new or a call among its arguments too, but not deeper nesting. A JVM's first compile of a null literal and a comparison, of a call of a method MVEL can't find on the type it gives the value, and of an assignment, and an assignment's first run, load classes MVEL compiles and runs them with too: deep in a stack, a first load could run out of stack where the same rule loads once one like it has been compiled, and an assignment's first run could fail its rule, so the warm-up compiles one of each, and runs all but the call, which would fail for the missing method (#1103). When the engine prepares MVEL, at build() or a first load, that warm-up initializes no class with a static initializer that it hasn't, so if it overflows, nothing is left unusable: it's skipped, and tried again by the next call, which the engine makes only for an engine whose builder names MVEL. A bare prepare() call in an otherwise empty JVM may also initialize JDK classes the reflective call needs. Anything else it throws is thrown. Once it has succeeded, in the class loader that loaded this module, later calls return at once; calls made before that, such as two first calls at once, each do the work.
      Specified by:
      prepare in interface ExpressionLanguage
    • reservedFactNames

      public Set<String> reservedFactNames()
      Returns "output", the name MVEL's actions see the output object by, which would hide a fact of the same name. Said here rather than left to the default, so a change to the default doesn't change MVEL.
      Specified by:
      reservedFactNames in interface ExpressionLanguage
      Returns:
      "output" alone
    • reservesForEveryRuleList

      public boolean reservesForEveryRuleList()
      Returns false: only MVEL's actions see the output object by "output", so the name is reserved only for a rule list with an MVEL rule, or with no rules while MVEL is the default language. A fact declared with it is then rejected by load() and validate() for such a rule list, rather than by build().
      Specified by:
      reservesForEveryRuleList in interface ExpressionLanguage
      Returns:
      false