Module io.github.brantunger.unruly
Package io.github.brantunger.unruly.mvel
Class MvelExpressionLanguage
java.lang.Object
io.github.brantunger.unruly.mvel.MvelExpressionLanguage
- All Implemented Interfaces:
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 keywordswith,def,functionorimport_static. A write made by calling a method, such asclaim.setApproved(true), can't be detected. - Variables an action declares stay local to that action, and assigning to
outputfails. - A fact name must be a Java identifier that isn't one of MVEL's reserved words, such as
emptyorin, and isn't a class name MVEL resolves instead, such asMathor an imported class, nor the first part of the name of a class the rule list's expressions name with its package, such asjavaforjava.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 afterisdefglued 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 asmy-factinmy-fact == 1ormy-fact-1, which MVEL reads asmyminusfact,\ain1-\a,my]inx^my]+=1,#ainisdef#aor,ain,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 oneisdefreads 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 afterisdefcounted 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 withRulesEngineBuilder.languageImports(String, String...),newCompiler(CompileContext)throwsIllegalArgumentException. The engine creates MVEL's compiler when a rule list given toload()orvalidate()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, andvalidate()returns, aRuleCompilationExceptionwith the throw as its cause. A rule list that doesn't use MVEL never has its imports checked.
- See Also:
-
Field Summary
Fields -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionname()Returns the language's name, such as"mvel".newCompiler(CompileContext context) Creates the compiler for one rule list, with the context's Java imports, class loader, declared facts and options.voidprepare()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, andBigIntegerandBigDecimal, which MVEL makes a rule's firstBigDecimalliteral, such as1.5B, with.Returns "output", the name MVEL's actions see the output object by, which would hide a fact of the same name.booleanReturnsfalse: 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.
-
Field Details
-
LANGUAGE_NAME
The language's name.- See Also:
-
-
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
Description copied from interface:ExpressionLanguageReturns 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 withServiceLoader, and knows the language by that name from then on.- Specified by:
namein interfaceExpressionLanguage- Returns:
- The name, never
nullor blank
-
newCompiler
Creates the compiler for one rule list, with the context's Java imports, class loader, declared facts and options.- Specified by:
newCompilerin interfaceExpressionLanguage- 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 haslanguage 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, andBigIntegerandBigDecimal, which MVEL makes a rule's firstBigDecimalliteral, such as1.5B, with. On JDK 25 and later,BigDecimal's static initializer initializes the JDK'sForkJoinPoolandForkJoinTasktoo, 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 aStackOverflowErrorinside 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 asMvelExpressionsets 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'sGetterAccessor, 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 noError, so one left unusable would fail every such call and everynewwithout 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,neworsoundslikeinitializes 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 withnullonce 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 thanrun()'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 anewor a call among its arguments too, but not deeper nesting. A JVM's first compile of anullliteral 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, atbuild()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 bareprepare()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:
preparein interfaceExpressionLanguage
-
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:
reservedFactNamesin interfaceExpressionLanguage- Returns:
- "output" alone
-
reservesForEveryRuleList
public boolean reservesForEveryRuleList()Returnsfalse: 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 byload()andvalidate()for such a rule list, rather than bybuild().- Specified by:
reservesForEveryRuleListin interfaceExpressionLanguage- Returns:
false
-