A script engine plugin for Fess, the open-source enterprise search server, that evaluates OGNL (Object-Graph Navigation Language) expressions.
Fess lets an administrator enter a small script or expression in several places — computing a
field value during crawling, deciding a document boost, or running a scheduled job. Fess 15.9
evaluates these with a JavaScript engine by default, and ships Groovy as a bundled plugin. This
plugin adds ognl as a further alternative for the places that just need a short expression,
not a script.
OGNL has no statements, no control flow, and no return; an OGNL expression is a single value
expression such as title + " - " + siteName or content.length() > 0. That constraint is the
point: an OGNL expression is easy to read at a glance, cannot loop or branch, and (in strict
mode) can be bounded to a known set of classes and members. Use it for field-mapping, boost and
path-mapping expressions. For a scheduled job — or anything else that needs statements, a loop or
a return — use one of the scripting engines instead.
Fess evaluates scripts in five places, and ognl can be selected in all five:
| Place | How |
|---|---|
| Data store crawling | Add the handler parameter script_type=ognl to the data crawling config |
| Web/File crawling field scripts | Add the config parameter config.script.type=ognl to the crawling config, and write the field templates as field.script.<field>=<expression> |
| Document boost (Boost Document Rule) | Set the rule's own Script Type to ognl. This is per rule — each boost rule chooses its own engine |
| Path mapping | Prefix the Replacement with ognl:. The expression's result becomes the whole replacement URL; the original URL and the regex Matcher are bound as #url and #matcher |
| Scheduler jobs | Set the job's Script Type to ognl. Possible, but rarely what you want — see below |
A scheduled job whose Script Type is ognl really is evaluated by this engine, and its result is
written to the job log. But a scheduled job is normally a procedure, and OGNL has no statements,
no control flow and no return. Every job Fess ships (Default Crawler, Suggest Indexer, Log
Purger, Thumbnail Generator, and the rest) is written against an engine that has those; none of
them can be rewritten as a single OGNL expression.
Note for readers of older documentation. Before Fess 15.9, the scheduler ignored the Script Type field and always used Groovy, and path mapping was tied to the Groovy engine. Both are fixed in 15.9: the scheduler honours the field, and path mapping resolves whatever engine name precedes the colon.
- Fess 15.9.0 or later
- Java 21 or later
Fess plugins are distributed through the CodeLibs Maven repository, not Maven Central. Download the plugin JAR from maven.codelibs.org.
- Download
fess-script-ognl-{version}.jar - Copy the JAR file to your Fess plugin directory (
$FESS_HOME/app/WEB-INF/plugin/) - Restart the Fess server
- The
ognlscript engine is now available wherever a script type can be selected (see Where OGNL Can Be Used above)
For general plugin installation steps, see the Plugin section of the Fess Administration guide.
The engine reads the following settings once, at Fess startup, from system.properties or from
the JVM system property -Dfess.system.<key> (system.properties takes precedence — the JVM
system property is consulted only when the key is absent from system.properties). Changing
any of these requires a Fess restart — they are not re-read at evaluation time.
| Key | Default | Notes |
|---|---|---|
script.ognl.mode |
compat |
compat or strict; see below |
script.ognl.cache.size |
1000 |
Maximum number of parsed expressions cached; clamped to a minimum of 1 |
script.ognl.max.log.length |
200 |
Maximum length of script text included in warning/error log messages; clamped to a minimum of 3 |
script.ognl.expression.max.length |
4000 |
Maximum number of characters an expression may contain; longer expressions are rejected (logged, evaluate to null); clamped to a minimum of 1 |
script.ognl.allowed.classes |
see strict mode | Comma-separated class/package allow list, used only in strict mode |
script.ognl.denied.packages |
see strict mode | Comma-separated declaring-class deny list, used only in strict mode |
A malformed numeric value (not a valid integer) is ignored and the default is used, with a warning logged. A value below the documented minimum is clamped up to that minimum, also with a warning logged.
compat (default) |
strict |
|
|---|---|---|
| Fess DI container | Bound into the expression as container |
Not bound — container is not reachable |
| Class allow list | Not applied — @any.Class@method(...) can reach any class |
Applied — only classes matching script.ognl.allowed.classes resolve |
| Member deny list | Not applied | Applied — members declared on a class matching script.ognl.denied.packages are not callable |
| Purpose | Preserves historical, pre-plugin-hardening behavior | Bounds what an expression can reach out to |
compat is the default and matches how this plugin has always behaved: it exists so existing data
store and crawler configurations keep working unchanged.
Setting script.ognl.mode=strict applies a class allow list (default: java.lang.Math,
java.lang.String, java.lang.Boolean, java.lang.Integer, java.lang.Long, java.lang.Float,
java.lang.Double, java.lang.Number, java.util.Date, java.util.Arrays, java.util.List,
java.util.Map, java.util.Set, java.util.Collections, java.math.BigDecimal, java.time
(and its sub-packages), org.codelibs.core.lang.StringUtil, org.codelibs.fess.util.DocumentUtil,
org.codelibs.fess.taglib.FessFunctions) and a declaring-class deny list (default: java.io,
java.nio, java.net, java.lang.reflect, java.lang.invoke, java.lang.System,
java.lang.Class, java.lang.Runtime, java.lang.ProcessBuilder, java.lang.Process,
java.lang.Thread, java.lang.ClassLoader, javax.script, jdk., sun., org.lastaflute.di).
If script.ognl.mode is set to anything other than compat or strict (including a typo), the
engine falls back to compat mode and logs a warning. The effective mode — along with the cache
size and expression max length actually in effect — is logged at INFO level on startup; check
that line to confirm strict mode actually took effect.
strict mode is not a sandbox for the objects an expression is handed — it only bounds what an
expression can reach out to. The class allow list controls which classes @some.Class@method(...)
can resolve, and the deny list blocks members declared on the listed classes/packages. Neither
list restricts what an expression can do with objects already present in its parameter map:
org.codelibs.* is not on the deny list, so if a caller puts a Fess helper or entity object into
the parameter map, every public method on that object remains callable from the expression, in
both modes. Treat strict mode as narrowing the blast radius of @Class@method(...) calls and
reflection-adjacent members — not as isolating the expression from the data it was given.
Data store crawling and field-mapping configs use the fieldName=expression form, one per line,
where expression is evaluated once per document per field:
title=title
content=content.trim()
url=url
digest=content.length() > 200 ? content.substring(0, 200) + "..." : content
Parameters are available in an expression both as a bare name and with the OGNL context-variable
prefix — name and #name resolve to the same value:
title=name
title=#name
OGNL is not a Groovy subset; expressions that look like Groovy can silently do the wrong thing. Watch for:
- No
return. An OGNL expression's value is the expression's value;return xis not valid OGNL syntax and fails to parse. - No statements or control flow. No
if/for/while, no multi-statement blocks. Use the ternary operator (cond ? a : b) for conditionals. - No Elvis operator (
?:).value ?: "default"is not supported. - No safe navigation (
?.).a?.bis not supported; a null-checking ternary (a != null ? a.b : null) is the equivalent. - Groovy-style string interpolation is not an error — it is worse.
"x${id}"is valid OGNL syntax, but OGNL does not interpolate it: it evaluates to the literal stringx${id}, including the unexpanded${id}. There is no exception or warning to catch the mistake; the field is simply indexed with the wrong text. - Numeric types differ from Groovy's.
10 / 3evaluates to theInteger3(integer division, like Java), and3.14evaluates to aDouble. Groovy would give aBigDecimalfor the literal and for the division. If an expression computes a boost value or is compared to aBigDecimal, this difference in type and in truncation behavior matters.
When Fess's audit log is enabled (script.audit.log.enabled, on by default), script execution is
recorded. Other engines log every evaluation. This engine does not: it logs the first
successful evaluation and the first failed evaluation of each distinct expression text, and
stays silent on repeats of the same text. This is intentional — an expression that runs once per
document per field would otherwise flood the audit log with repeats of the same text and add
nothing.
Only the web application process is audited. Audit records come from Fess's
activityHelper, which is a web application component; the crawler process has neither that component nor an audit appender. So a scheduled job with Script Typeognlis recorded, while data store, field-mapping and boost expressions — which all run inside the crawler process — produce no audit records at all. Do not treat an empty audit log as evidence that a crawl's expressions did not run.
"Failed evaluation" here means an expression that parsed successfully but failed while it ran
(for example, 1 / 0). An expression that fails to parse is not audited at all, at any point:
parse failures are not added to the parsed-expression cache, so there is no cached entry to track
dedup state against, and auditing them would flood the log with the same parse error on every
evaluation of that expression instead of deduplicating it.
This guarantee is scoped to the parsed-expression cache, not to the lifetime of the process: an
expression is deduplicated only while it remains in the cache described by
script.ognl.cache.size. If it is evicted (the cache is bounded and least-recently-used entries
are dropped once it is full) or the engine is reinitialized (e.g., a restart), the next evaluation
of that same expression text is treated as new and logged again.
script.ognl.allowed.classes matches on a dot-separated prefix: an entry like java.time allows
java.time.LocalDate and any class under the java.time package. It does not allow nested
(inner) classes reachable through an enclosing class or package prefix — a nested class's binary
name uses $, not . (for example, java.util.Map$Entry), so a prefix such as java.util.Map
does not admit Map.Entry. If an expression needs a specific nested class in strict mode, add
its full binary name (with $) to script.ognl.allowed.classes explicitly.
This plugin pins to OGNL 3.4.7 deliberately; do not bump it without checking this first. OGNL
3.4.9 and later removed the null-chain short-circuit in property access: evaluating a.b.c when
a.b is null returns null on 3.4.7, but throws on 3.4.9+. Fess data store expressions
routinely read fields that may be absent on a given document, producing exactly this pattern of
null intermediate values. Upgrading past 3.4.7 would turn ordinary missing fields into warnings
and dropped values across otherwise unremarkable documents.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.