Class RulesEngineBuilder<O>
- Type Parameters:
O- The type of the output object
RulesEngine. An engine's languages, imports, listeners, limit on compiled copies,
copies made at load, run timeout and clock are set here and can't change once it's built; only its rules can, with
RulesEngine.load(List).
Compiled copies: a run uses a copy of the rules that no other run is using, and an engine keeps as many as
the most runs it has had in progress at once. A thread pool bounds that; virtual threads don't, so by default an
engine limits runs on virtual threads to one copy for every two processors. maxCopies(int) sets a
limit for every kind of thread, and unlimitedCopies() turns it off, leaving only build
slots to pace new copies on virtual threads. See
Compiled copies.
RulesEngine<LoanDecision> engine = RulesEngineBuilder.firstMatch(LoanDecision::new)
.imports("java.time")
.listener(new LoggingRuleListener())
.build();
engine.load(rules);
Languages: an engine built without language(ExpressionLanguage) has the languages found with
ServiceLoader, such as MVEL from the unruly-engine artifact. An engine built with it has
exactly the languages given. A rule whose language is null is written in the engine's default language:
the one named with defaultLanguage(String), or else the engine's only language.
A builder isn't thread-safe. build() can be called more than once, and each call builds a new engine with
the settings at that moment.
- See Also:
-
Method Summary
Modifier and TypeMethodDescriptionstatic <O> RulesEngineBuilder<O> allMatches(Supplier<O> outputFactory) Starts building an engine that fires the action of every rule whose condition is true, in priority order, all changing the same output object: DMN's rule order.build()Builds an engine with this builder's settings.Sets the clock a run reads, once when it starts, to decide which rules are within their validity window (Rule.getValidFrom(),Rule.getValidTo()).copiesAtLoad(int count) Makescountcompiled copies of the rules each timeRulesEngine.load(List)runs, so the first runs borrow copies that are ready instead of making them.defaultLanguage(String name) Names the language of the rules whoselanguageisnull.Declares a fact's name and type, so the engine checks a run's value against the type and a language can check its expressions against it.Declares several facts at once, asfact(String, Class)does one.static <O> RulesEngineBuilder<O> firstMatch(Supplier<O> outputFactory) Starts building an engine that fires the action of the highest-priority rule whose condition is true: the first match, in DMN's terms.Adds imports that every language compiles rules with, so rule expressions can refer to classes by their simple names.imports(Collection<String> names) Adds imports, asimports(String...)does.language(ExpressionLanguage language) Adds an expression language that rules can be written in, chosen by each rule'slanguage.languageImports(String language, String... names) Adds imports for one expression language only, which it reads fromCompileContext.languageImports()when rules are loaded: as written, in order, duplicates included.languageImports(String language, Collection<String> names) Adds imports for one expression language only, aslanguageImports(String, String...)does.listener(RuleListener listener) Adds a listener that is told about every condition evaluated and every action run.listeners(Collection<? extends RuleListener> listeners) Adds listeners, in the order given, aslistener(RuleListener)does.maxCopies(int maxCopies) Keeps at mostmaxCopiescompiled copies of the rules, for runs on every kind of thread, instead of the default limit on runs from virtual threads.Sets an option for one expression language, which the language reads fromCompileContext.options()when rules are loaded.outputType(Class<? super O> type) Tells expression languages the type of the output object, throughCompileContext.outputType().outputWriter(OutputWriter<? super O> writer) Sets how the engine sets the properties an action returns withActionResult.set(Map)on the output object.Rejects a run that supplies a fact nobody declared, or leaves a declared fact out.runTimeout(Duration timeout) Stops a run that is still going aftertimeout.static <O> RulesEngineBuilder<O> uniqueMatch(Supplier<O> outputFactory) Starts building an engine that fires the action of the one rule whose condition is true, and fails the run when more than one is: DMN's unique match, for a decision table whose rows must not overlap.Makes as many compiled copies of the rules as the runs in progress need, on any kind of thread, instead of the default limit on runs from virtual threads.
-
Method Details
-
firstMatch
Starts building an engine that fires the action of the highest-priority rule whose condition is true: the first match, in DMN's terms. Rules with equal priorities keep their list order.Conditions are evaluated in that order, and the run stops at the first match, so the rules below it are never evaluated: they're neither matched nor unmatched, the run's
resultreports them as not evaluated, and a broken condition among them can't fail a run that's already decided. UseallMatches(Supplier)oruniqueMatch(Supplier)when every condition must be evaluated. A rule the run skips is never evaluated, and is reported as skipped wherever it is; seeRule.- Type Parameters:
O- The type of the output object- Parameters:
outputFactory- Creates the output object. It is called once per run that matches a rule and must return a new, non-null object each time.- Returns:
- A new builder
- Throws:
NullPointerException- ifoutputFactoryisnull
-
allMatches
Starts building an engine that fires the action of every rule whose condition is true, in priority order, all changing the same output object: DMN's rule order. Every condition, except those of rules the run skips, is evaluated before any action runs.- Type Parameters:
O- The type of the output object- Parameters:
outputFactory- Creates the output object. It is called once per run that matches a rule and must return a new, non-null object each time.- Returns:
- A new builder
- Throws:
NullPointerException- ifoutputFactoryisnull
-
uniqueMatch
Starts building an engine that fires the action of the one rule whose condition is true, and fails the run when more than one is: DMN's unique match, for a decision table whose rows must not overlap. Every condition, except those of rules the run skips, is evaluated, in priority order, before anything fires, so the failure names every rule that matched.No match returns
null, like the other engines. More than one match fires nothing, doesn't call the output supplier, and throws aRuleExecutionExceptionthat names each matched rule in priority order and belongs to no rule: listeners getafterEvaluatefor every rule the run doesn't skip, thenonRunError. It depends on nothing language-specific: it counts the conditions that were true.- Type Parameters:
O- The type of the output object- Parameters:
outputFactory- Creates the output object. It is called once per run that matches exactly one rule and must return a new, non-null object each time.- Returns:
- A new builder
- Throws:
NullPointerException- ifoutputFactoryisnull
-
language
Adds an expression language that rules can be written in, chosen by each rule'slanguage. Once this is called, the engine has exactly the languages added, and finds none withServiceLoader.The language's
ExpressionLanguage.name()is read once, here, and the engine knows the language by that name from then on.Adding a language initializes no class of the engine's, nor, whatever its name's characters, one of the JDK's character tables, as
build()first makes room on the stack for that (#1292). A language this rejects is the exception: rejecting it initializes the engine's small class that names the problem, before any room is made, so a rejected language added deep in a stack can still leave that class unusable for the life of the JVM. On Linux and macOS, so can the JVM's first read of a string with a character above U+00FF, which initializesjava.lang.StringUTF16: a name with such a character may be that read, if the JVM hasn't read such a string already (#1295).- Parameters:
language- The language- Returns:
- This builder
- Throws:
IllegalArgumentException- if the language's name isnullor blank, or a language with the same name was added alreadyNullPointerException- iflanguageisnull
-
defaultLanguage
Names the language of the rules whoselanguageisnull. Fact names are checked against it too when the loaded rule list is empty. Without this, the default is the engine's only language.- Parameters:
name- The name of one of the engine's languages- Returns:
- This builder
- Throws:
NullPointerException- ifnameisnull
-
imports
Adds imports that every language compiles rules with, so rule expressions can refer to classes by their simple names. Each string is a fully qualified package name ("java.util") or class name ("java.time.LocalDate", or"java.util.Map.Entry"for a nested class). A language without imports ignores them. Imports that aren't Java classes or packages, such as a JavaScript module, are given to their language alone, withlanguageImports(String, String...).Whether a string names a class is decided by
build(), with the building thread's context class loader, or this library's class loader if the thread has none. A string must be a valid package name, each dot-separated part a Java identifier, even to name a class; one that loader doesn't find as a class is imported as a package. Classes in imported packages are looked up each time rules are loaded, with the loading thread's context class loader.An import may have at most 1,000 characters and at most 64 dot-separated parts.
build()checks both, and the import's shape, before it looks the import up, and rejects one that fails any of them.- Parameters:
names- Package or class names- Returns:
- This builder
- Throws:
NullPointerException- ifnamesor any element isnull; nothing is added
-
imports
Adds imports, asimports(String...)does.- Parameters:
names- Package or class names- Returns:
- This builder
- Throws:
NullPointerException- ifnamesor any element isnull; nothing is added
-
languageImports
Adds imports for one expression language only, which it reads fromCompileContext.languageImports()when rules are loaded: as written, in order, duplicates included. The engine doesn't resolve them, and its other languages don't see them. What an import means, such as a module, a file or a namespace, is up to the language, which can reject one it can't use by throwing fromnewCompiler. The engine creates a language's compiler when a rule list given toRulesEngine.load(List)orRulesEngine.validate(List)has a rule in that language, and creates the default language's compiler for a list with no rules too. A throw fromnewCompilerthen fails that rule list:load()throws, andvalidate()returns, aRuleCompilationExceptionwith the throw as its cause. A language no such rule list uses never has its imports checked. Adding imports for the same language again adds them after the others.An import may have at most 1,000 characters, which
build()checks.build()also fails iflanguageisn't one of the engine's languages, as it does foroption(...). A call with no names still records the language, sobuild()fails for it all the same.- Parameters:
language- The name of one of the engine's languagesnames- The imports, as the language writes them, such as"lodash/fp"- Returns:
- This builder
- Throws:
NullPointerException- iflanguage,namesor any element isnull; nothing is added
-
languageImports
Adds imports for one expression language only, aslanguageImports(String, String...)does.- Parameters:
language- The name of one of the engine's languagesnames- The imports, as the language writes them- Returns:
- This builder
- Throws:
NullPointerException- iflanguage,namesor any element isnull; nothing is added
-
listener
Adds a listener that is told about every condition evaluated and every action run.- Parameters:
listener- The listener- Returns:
- This builder
- Throws:
NullPointerException- iflistenerisnull
-
listeners
Adds listeners, in the order given, aslistener(RuleListener)does.- Parameters:
listeners- The listeners- Returns:
- This builder
- Throws:
NullPointerException- iflistenersor any element isnull; nothing is added
-
outputType
Tells expression languages the type of the output object, throughCompileContext.outputType(). A language may use it, for example to check the properties its actions return; the engine doesn't. Without this, languages are toldObject.- Parameters:
type- The output object's class, or a supertype of it, such asMap.classfor aMap<String, Object>output- Returns:
- This builder
- Throws:
NullPointerException- iftypeisnull
-
outputWriter
Sets how the engine sets the properties an action returns withActionResult.set(Map)on the output object. Without this, it'sOutputWriter.beansAndMaps(). Actions that change the output themselves don't use it. The engine callsOutputWriter.set(Object, String, Object, OutputConverter), so a writer that overrides it can have the rule's language convert a value for the type it needs; one written as a lambda gets the value as the action returned it.- Parameters:
writer- The writer, which runs on many threads at once- Returns:
- This builder
- Throws:
NullPointerException- ifwriterisnull
-
option
Sets an option for one expression language, which the language reads fromCompileContext.options()when rules are loaded. What the options are is up to the language. Setting the same key again replaces its value.- Parameters:
language- The name of one of the engine's languageskey- The option's namevalue- Its value- Returns:
- This builder
- Throws:
NullPointerException- if an argument isnull
-
fact
Declares a fact's name and type, so the engine checks a run's value against the type and a language can check its expressions against it.The engine fails a run with
IllegalArgumentExceptionwhen the fact is present and its value isn't an instance oftype, or, for a primitive type, its wrapper or a boxed primitive Java widens to it, as described below. Anullvalue passes: nothing about it contradicts the declaration. So a fact declared with a primitive type can still benull: the engine doesn't turn it into the primitive's default, andOutputWriter.beansAndMaps()never passes it to a setter taking a primitive, so it fails withIllegalArgumentExceptionwhere no other setter of the property takesnull. What a language makes of it is up to the language; see Declaring facts. A run that doesn't supply the fact at all is unaffected, unlessrequireDeclaredFacts()is also set. Declaring a fact twice replaces the first declaration.A primitive type accepts its wrapper, and any boxed primitive that Java widens to it (JLS 5.1.2), lossy conversions included:
fact("n", long.class)accepts anIntegeras well as aLong. The engine converts such a value to the declared type's wrapper before the run starts, so listeners, fromRuleListener.beforeRun(RunContext)on, and every language see aLong; the fact store keeps theInteger. Nothing is narrowed, aBooleanconverts to nothing else, and aBigDecimalorBigIntegeris never converted. A wrapper type converts nothing:fact("n", Long.class)rejects anInteger, as Java never converts one wrapper to another. Languages are told the wrapper, such asLong.RulesEngine.load(List)checks every declared name with the language of each rule, as a run checks the names it's given, so a name no language can refer to fails loading rather than every run.Languages are told what was declared, and use it as they can, for example to check their expressions when the rules load; the engine's own check happens whatever the language does. See each language's documentation.
Declaring a fact initializes no class of the engine's, nor, whatever the name's characters, one of the JDK's character tables, as
build()first makes room on the stack for that (#1292). A name this rejects is the exception: rejecting it initializes the engine's small class that names the problem, before any room is made, so a rejected name declared deep in a stack can still leave that class unusable for the life of the JVM. On Linux and macOS, so can the JVM's first read of a string with a character above U+00FF, which initializesjava.lang.StringUTF16: a name with such a character may be that read, if the JVM hasn't read such a string already (#1295).- Parameters:
name- The fact's name, as rules refer to ittype- The type a run's value must be an instance of, or, for a primitive type, be widened to. DeclaringObjector aMapsays the fact's shape isn't fixed, which no language can type-check- Returns:
- This builder
- Throws:
NullPointerException- ifnameortypeisnullIllegalArgumentException- ifnameis blank. A name one of the engine's languages reserves, such as "output", which actions use for the output object by default (seeExpressionLanguage.reservedFactNames()), is rejected bybuild(), once the languages are known; or, if the language reserves it only for the rule lists that use it (ExpressionLanguage.reservesForEveryRuleList()), by theload()of such a rule list, whichvalidate()reports too
-
facts
Declares several facts at once, asfact(String, Class)does one.- Parameters:
types- The type of each fact, by name; copied, so later changes to the map don't change the engine- Returns:
- This builder
- Throws:
NullPointerException- iftypes, a name or a type isnull; nothing is declaredIllegalArgumentException- if a name is blank; nothing is declared. A name one of the engine's languages reserves is rejected bybuild(), or byload(), as forfact(String, Class)
-
requireDeclaredFacts
Rejects a run that supplies a fact nobody declared, or leaves a declared fact out. Without this, declaring a fact only says what its type is when it's there.It says that
fact(String, Class)lists every fact a run may supply, which is what lets a language reject an expression that refers to anything else, when the language offers that and it's turned on: for MVEL, itsstrongTypingoption, which also needs this. The engine's own checks on a run's facts don't depend on any language.- Returns:
- This builder
-
maxCopies
Keeps at mostmaxCopiescompiled copies of the rules, for runs on every kind of thread, instead of the default limit on runs from virtual threads.A run that starts while all of them are in use waits until one is free, so at most
maxCopiesruns make progress at once. A run started from inside another run on the same thread, and a run that has waited five seconds without one single copy being given back, don't wait: each gets an extra copy that isn't kept. A run that is interrupted while it waits, or that passes its deadline there, throws aRuleExecutionException; an interrupted thread keeps its interrupt status set. A rule list whose languages all returnSession.none()needs no copies, and no limit applies to it.The limit bounds the copies of each rule list, and the runs holding a copy across reloads: a run still using rules that
RulesEngine.load(List)replaced holds its copy under the same limit as runs on the new rules. The replaced rules also keep copies given back idle for their runs still waiting for one, at most one for each such run and never more thanmaxCopies, which hold no place under the limit. So for a short time after a reload the engine can have up tomaxCopiesmore copies for each replaced rule list that still has runs waiting, until the last run on those rules has finished.- Parameters:
maxCopies- The most compiled copies of the rules to keep, and so the most runs making progress at once; at least 1- Returns:
- This builder
- Throws:
IllegalArgumentException- ifmaxCopiesis less than 1- See Also:
-
unlimitedCopies
Makes as many compiled copies of the rules as the runs in progress need, on any kind of thread, instead of the default limit on runs from virtual threads.Like an engine before 2.0, its runs never wait for a copy, which suits a thread pool, whose size bounds the copies. On a virtual thread, a run that finds no idle copy, while no run on that thread holds or is getting a copy, first waits for one of the engine's build slots, one for each processor, and holds it until the new copy's first run ends, which is when a language such as MVEL compiles the expressions. The slots pace the new copies without bounding them: the copies still grow with the runs in progress, so rules that wait — on I/O, a database or another service — can make many. In a language such as MVEL, each copy recompiles every expression and generates its own accessor classes. For a hard bound, use
maxCopies(int)sized for the runs you want waiting at once; on JDK 21 to 23, with a language whose expressions contend on a lock the whole JVM shares, such as MVEL, below the number of carriers, since a limit at or above it can deadlock every carrier there, and so can this.A run with a deadline waits for a slot for at most half the time it has left, and a run that sees no slot given back for five seconds stops waiting; either then makes its copy without a slot, so no run fails for want of a copy. A run interrupted while it waits throws a
RuleExecutionException, and its thread keeps its interrupt status set. Runs on platform threads never wait.- Returns:
- This builder
- See Also:
-
copiesAtLoad
Makescountcompiled copies of the rules each timeRulesEngine.load(List)runs, so the first runs borrow copies that are ready instead of making them. Without this, no copy is made until a run needs one.Making a copy compiles the rules again, in each language that keeps state between runs, and prepares its sessions with
warmUp: a language such as MVEL compiles every condition and action into each session.load()makes the copies one after another on its own thread, after compiling the rules and before runs can see them, so runs go on using the rules it replaces meanwhile, andload()takes that much longer: Making copies at load gives a measurement. Rules whose languages all keep no state between runs get one set of sessions that every run shares, whatevercountis.It's for runs on virtual threads on JDK 24 and later, where a run that makes a copy can keep its carrier while a language that loads classes as it compiles, such as MVEL, loads them. It doesn't move what a language does during a copy's first runs, such as MVEL generating accessor classes, and a copy made during a run, such as one for a run nested in another, is made as before. See Class loading pins carriers.
- Parameters:
count- How many copies to make; zero, the default, makes none. WithmaxCopies(int), it can't be more than the limit, since no more copies than that are used at once. The default limit, on runs from virtual threads only, andunlimitedCopies()allow any number.- Returns:
- This builder
- Throws:
IllegalArgumentException- ifcountis negative;build()throws it ifcountis more thanmaxCopies(int)
-
runTimeout
Stops a run that is still going aftertimeout. Without this, a run has no deadline.The deadline is taken from when
RulesEngine.run(FactStore)is called, and waiting for a compiled copy of the rules counts towards it. The engine checks it before each condition and each action, again when each one returns, and once theOutputWriterhas set the properties an action returned, so a run whose last condition or action returns past its deadline fails even though that rule finished. It doesn't stop an expression that is already running, nor interrupt the thread; only a language that checksEvaluationContext.isCancelled(), or registers an action withEvaluationContext.onCancel(Runnable)that stops its runtime at the deadline, can stop inside one. A run past its deadline throws aRuleExecutionExceptioncaused by aTimeoutException.- Parameters:
timeout- How long a run may take; positive.RunOptions.withTimeoutOf(Duration)gives a single run one instead.- Returns:
- This builder
- Throws:
IllegalArgumentException- iftimeoutis zero or negativeNullPointerException- iftimeoutisnull- See Also:
-
clock
Sets the clock a run reads, once when it starts, to decide which rules are within their validity window (Rule.getValidFrom(),Rule.getValidTo()). Without this, the engine usesClock.systemUTC(). A test can give a fixed clock, such asClock.fixed(Instant.parse("2027-06-01T00:00:00Z"), ZoneOffset.UTC), to run the rules as they're scheduled at that time.Only the validity window uses the clock. A
run timeoutis measured with a monotonic clock whatever clock is set here, so a step of the system clock doesn't move a running deadline. Anything the clock throws fails the run unchanged, before any listener is told the run started, and isn't logged; anullinstant fails it the same way, with aNullPointerException.The instant a run read is its
RunContext.startedAt(), which listeners get, and itsRunResult.startedAt(), so an audit record can say what time judged the windows. Its conditions and actions read the same instant asEvaluationContext.startedAt(), so a condition that compares against the current time agrees with the windows, and with a fixed clock.- Parameters:
clock- The clock- Returns:
- This builder
- Throws:
NullPointerException- ifclockisnull
-
build
Builds an engine with this builder's settings. Load its rules withRulesEngine.load(List)before the first run.- Returns:
- A new engine
- Throws:
IllegalStateException- if the engine has no expression language; if it has several and nodefault language; if the default language, a language given anoption, or one givenimports, isn't one of its languages; if a language found withServiceLoaderhas anullor blank name, or two found languages have the same name; or if one of the engine's languages, given or found, returnsnull, or a set holdingnull, fromExpressionLanguage.reservedFactNames(). AnythingServiceLoaderor a language throws while it's found, such as aServiceConfigurationError, is thrown unchanged, and so is anything a language'sreservedFactNames()orreservesForEveryRuleList()throws.IllegalArgumentException- if a fact isdeclaredwith a name one of the engine's languages, given or found, reserves for every rule list (seeExpressionLanguage.reservedFactNames()andExpressionLanguage.reservesForEveryRuleList()); if a language's own import has more than 1,000 characters; if an import has more than 1,000 characters or more than 64 dot-separated parts, or has a dot-separated part that isn't a Java identifier ('...' is neither a class nor a valid package name, with no cause), all checked before any class lookup, so even a class of that name fails; if it names a class that exists but can't be loaded, for example because a class it depends on is missing; or ifcopiesAtLoad(int)is more thanmaxCopies(int)StackOverflowError- if this is the JVM's first engine and the calling thread has too little stack left to build it safely. The first engine initializes the classes engines use to build, load, validate, run and close, and the room for that is checked first, before any of them is touched and before any setting is checked, so a first engine built too deep with wrong settings throws this, not their exception; none is left unusable, and the nextbuild()checks again. Thrown too, before any language is prepared, if a language this builder names, withdefaultLanguage(String),language(io.github.brantunger.unruly.api.language.ExpressionLanguage)or settings for it, is of a class no engine has prepared yet (seeExpressionLanguage.prepare()) and too little stack is left to prepare it
-