Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions .travis.yml

This file was deleted.

78 changes: 50 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,41 @@

A simple example plugin demonstrating how to create custom script engines for [Fess](https://fess.codelibs.org/), the open-source enterprise search server.

## What Is a Script Engine?

A Fess script engine turns a **template** plus a **parameter map** into a value. Fess uses script
engines wherever an administrator can enter a small script or expression — for example to compute a
document boost, derive a field value during crawling, or run logic in a scheduled job. The built-in
`groovy` engine evaluates full Groovy scripts; this example shows the minimal shape of a custom one.

## Overview

This plugin provides a minimal implementation of a custom script engine for Fess. The `ExampleEngine` serves as a template and starting point for developers who want to create their own script engines with custom template processing logic.
This plugin provides a minimal implementation of a custom script engine for Fess. The `ExampleEngine`
serves as a template and starting point for developers who want to create their own script engines
with custom template processing logic.

### Key Features

- **Simple Implementation**: Demonstrates the basic structure of a Fess script engine
- **Template Pass-through**: Returns template strings unchanged (useful for testing and learning)
- **Full Integration**: Properly integrated with Fess's dependency injection container
- **Comprehensive Tests**: Includes extensive test cases covering edge cases and various scenarios
- **Real, Minimal Evaluation**: Performs simple `${key}` placeholder substitution from the parameter map
- **Idiomatic Structure**: Demonstrates the standard structure of a Fess script engine
- **Self-Registration**: Registers itself with the script engine factory via the DI container
- **Focused Tests**: Meaningful tests covering substitution, missing keys, null/blank input, and factory lookup

## Architecture

The plugin extends Fess's `AbstractScriptEngine` class and implements:

- **Template Evaluation**: Process template strings with parameter maps
- **Engine Identification**: Provides a unique name ("example") for the script engine
- **DI Integration**: Configured via LastaDi container for seamless Fess integration
- **Template Evaluation** (`evaluate`): Substitutes `${key}` placeholders with values from the parameter map.
A blank template returns `null`; a missing or `null` value leaves the placeholder untouched.
- **Engine Identification** (`getName`): Provides the unique name (`"example"`) used to register and look up the engine
- **DI Integration**: `fess_se++.xml` registers the engine into Fess's `scriptEngineFactory` at startup
via a `register` postConstruct (the `++` suffix means the fragment is additively merged into the core `fess_se.xml`)

## Installation

### Prerequisites

- Fess 15.0.0 or later
- Fess 15.7.0 or later
- Java 21 or later

### Download
Expand All @@ -42,20 +53,28 @@ You can download the plugin JAR from [Maven Central](https://repo1.maven.org/mav
3. Restart Fess server
4. The "example" script engine will be available for use

For detailed installation instructions, see the [Fess Plugin Guide](https://fess.codelibs.org/15.0/admin/plugin-guide.html).
For detailed installation instructions, see the [Fess Plugin Guide](https://fess.codelibs.org/15.7/admin/plugin-guide.html).

## Usage

Once installed, you can use the "example" script engine in your Fess configuration:
There is **no extra configuration to "use" the engine** — the plugin self-registers via
`fess_se++.xml` when Fess starts. Once installed, the engine is available by its registered name,
`example`, anywhere Fess lets you pick a script type, including:

- **Data store crawling** — field-mapping scripts that compute index field values
- **Document boost** — boost expressions evaluated per document during crawling
- **Scheduled jobs** — jobs whose *Script Type* is set to an engine name
- **Path mappings / replacements** — value transformations that accept a script type

In those places, select or enter `example` as the script type and provide a template such as
`Hello ${name}`. Internally Fess resolves the engine through the factory:

```xml
<component name="exampleScriptEngine" class="org.codelibs.fess.script.example.ExampleEngine"/>
```java
ComponentUtil.getScriptEngineFactory().getScriptEngine("example").evaluate(template, paramMap);
```

The engine will process templates by returning them unchanged, making it useful for:
- Testing script engine integration
- Learning how to implement custom script engines
- As a starting point for more complex implementations
This example engine substitutes `${key}` placeholders with values from the parameter map, which
makes it a useful starting point for learning the API and for building richer engines.

## Development

Expand All @@ -73,12 +92,11 @@ mvn clean package
mvn test
```

The test suite includes 19 comprehensive test cases covering:
- Basic functionality
- Edge cases (null/empty inputs)
- Various data types and special characters
- Multi-line templates and large content
- Instance independence and data integrity
The test suite includes focused test cases covering:
- Placeholder substitution (single, multiple, and non-string values)
- Missing-key and null-value behavior (placeholder left untouched)
- Blank/null template handling (returns `null`)
- Engine name and factory registration/lookup by name

### Code Quality

Expand All @@ -99,12 +117,16 @@ mvn javadoc:javadoc
src/
├── main/java/
│ └── org/codelibs/fess/script/example/
│ └── ExampleEngine.java # Main script engine implementation
│ └── ExampleEngine.java # Script engine implementation (${key} substitution)
├── main/resources/
│ └── fess_se++.xml # DI container configuration
└── test/java/
└── org/codelibs/fess/script/example/
└── ExampleEngineTest.java # Comprehensive test suite
│ └── fess_se++.xml # DI fragment that registers the engine (additive merge)
└── test/
├── java/
│ └── org/codelibs/fess/script/example/
│ ├── ExampleEngineTest.java # Engine tests + factory lookup
│ └── UnitScriptTestCase.java # Minimal UTFlute test base for the plugin
└── resources/
└── test_app.xml # Test DI container (includes scriptEngineFactory)
```

## Creating Your Own Script Engine
Expand Down
65 changes: 57 additions & 8 deletions src/main/java/org/codelibs/fess/script/example/ExampleEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,36 @@
*/
package org.codelibs.fess.script.example;

import java.util.Collections;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.codelibs.core.lang.StringUtil;
import org.codelibs.fess.script.AbstractScriptEngine;

/**
* Example script engine implementation that demonstrates how to create custom script engines for Fess.
* This implementation simply returns the template string unchanged without any processing.
* Example script engine that demonstrates how to implement a custom script engine for Fess.
*
* <p>A script engine takes a {@code template} string plus a {@code paramMap} of named values
* and produces a result. Fess invokes engines through {@link AbstractScriptEngine}: this example
* registers itself under the name {@code "example"} (see {@link #getName()}) and is wired into the
* DI container by {@code fess_se++.xml}.</p>
*
* <p>To keep the example easy to follow, this engine performs simple {@code ${key}} placeholder
* substitution: every {@code ${key}} occurrence in the template is replaced with the matching
* value from {@code paramMap}. For example, the template {@code "Hello ${name}"} with
* {@code {name=Fess}} evaluates to {@code "Hello Fess"}.</p>
*
* <p>When you build your own engine, the two things you customize are the evaluation logic in
* {@link #evaluate(String, Map)} and the engine identifier in {@link #getName()}. The real
* {@code GroovyEngine} in Fess follows the same shape but evaluates full Groovy scripts.</p>
*/
public class ExampleEngine extends AbstractScriptEngine {

/** Matches {@code ${key}} placeholders where {@code key} is one or more non-"}" characters. */
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");

/**
* Creates a new instance of ExampleEngine.
*/
Expand All @@ -33,21 +53,50 @@ public ExampleEngine() {
}

/**
* Evaluates the given template with the provided parameter map.
* In this example implementation, the template is returned unchanged without any processing.
* Evaluates the given template by substituting {@code ${key}} placeholders with values
* from the parameter map.
*
* <p>Null-safety mirrors {@code GroovyEngine}: a blank template returns {@code null}, and a
* {@code null} parameter map is treated as an empty map. A {@code ${key}} whose key is missing
* from the map (or maps to {@code null}) is left untouched in the output, so unresolved
* placeholders remain visible rather than being silently dropped.</p>
*
* @param template the template string to evaluate
* @param paramMap the parameter map containing variables for template evaluation
* @return the template string unchanged
* <p>CUSTOMIZE HERE: replace this substitution logic with whatever evaluation your engine
* needs (a real scripting language, an expression evaluator, an external template engine,
* etc.).</p>
*
* @param template the template string to evaluate (null-safe, returns null if blank)
* @param paramMap the parameters available to the template (null-safe, treated as empty if null)
* @return the evaluated string, or null if the template is blank
*/
@Override
public Object evaluate(final String template, final Map<String, Object> paramMap) {
return template;
if (StringUtil.isBlank(template)) {
return null;
}

final Map<String, Object> safeParamMap = paramMap != null ? paramMap : Collections.emptyMap();

final Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
final StringBuilder buffer = new StringBuilder();
while (matcher.find()) {
final String key = matcher.group(1);
final Object value = safeParamMap.get(key);
// Leave the original "${key}" in place when the key is missing or null.
final String replacement = value != null ? value.toString() : matcher.group();
matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(buffer);
return buffer.toString();
}

/**
* Returns the name of this script engine.
*
* <p>CUSTOMIZE HERE: this is the identifier the engine is registered and looked up under
* (e.g. via {@code ScriptEngineFactory.getScriptEngine("example")}). Change it to your own
* engine's unique name.</p>
*
* @return the name "example" that identifies this script engine
*/
@Override
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/fess_se++.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//DBFLUTE//DTD LastaDi 1.0//EN"
"http://dbflute.org/meta/lastadi10.dtd">
<!--
The "++" suffix marks this as an additive-merge fragment: LastaDi merges its
components into the existing fess_se.xml definitions instead of replacing them,
so this plugin can contribute a new engine without redefining the factory.

The "register" postConstruct calls AbstractScriptEngine#register(), which adds
this engine to the scriptEngineFactory under ExampleEngine#getName() ("example").
-->
<components>
<component name="exampleEngine"
class="org.codelibs.fess.script.example.ExampleEngine">
Expand Down
Loading
Loading