Skip to content

Commit bfe1a42

Browse files
authored
TIKA-4856 presets (#3118)
1 parent b5c52d4 commit bfe1a42

28 files changed

Lines changed: 1414 additions & 23 deletions

File tree

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
Release 4.1.0 - unreleased
22

3+
* tika-server: named configuration presets (TIKA-4856).
4+
35
* Temp files follow -Djava.io.tmpdir on the parent JVM (Tika, its
46
libraries, and forks all honor it); TikaLoader fails at config load
57
if it is unusable. pipes.tempDirectory is deprecated for removal in

docs/modules/ROOT/pages/pipes/configuration.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ See also xref:pipes/timeouts.adoc[Timeouts] for the full timeout model.
136136

137137
|`maxTotalTaskTimeoutMillis`
138138
|`3600000`
139-
|Ceiling for *request-supplied* timeout limits: a per-request `timeout-limits` override may lower its timeouts freely but can never raise `totalTaskTimeoutMillis` or `progressTimeoutMillis` above this value (values over the cap are clamped with a warning). Limits set in the server's own `parse-context` are trusted and not subject to this cap.
139+
|Ceiling for *request-supplied* timeout limits: a per-request `timeout-limits` override may lower its timeouts freely but can never raise `totalTaskTimeoutMillis` or `progressTimeoutMillis` above this value (values over the cap are clamped with a warning). Limits set in the server's own `parse-context` — or in a preset a request selects — are trusted and not subject to this cap.
140140

141141
|`maxWaitForClientMillis`
142142
|`60000`

docs/modules/ROOT/pages/pipes/plugins/json.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ Unlike the other iterators, this one takes no `fetcherId` / `emitterId`: each li
5757
== Notes
5858

5959
* The file format is JSON-lines (also called NDJSON) — one valid JSON object per line, no surrounding array brackets.
60-
* Each line must be a serialized `FetchEmitTuple`: `id`, `fetcher`, `fetchKey`, `emitter`, `emitKey`, and optionally `fetchRangeStart`, `fetchRangeEnd`, `metadata`, `parse-context`, `onParseException`. Any other
60+
* Each line must be a serialized `FetchEmitTuple`: `id`, `fetcher`, `fetchKey`, `emitter`, `emitKey`, and optionally `fetchRangeStart`, `fetchRangeEnd`, `metadata`, `parse-context`, `onParseException`, `preset`. Any other
6161
field is rejected — the deserializer does not tolerate unknown keys.
6262
* For columnar work items in a CSV, use the xref:pipes/plugins/csv.adoc[CSV iterator] instead.

docs/modules/ROOT/pages/using-tika/server/index.adoc

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,62 @@ WARNING: Enable this only behind network controls (firewalls, private subnets),
105105
or xref:using-tika/server/tls.adoc[2-way TLS authentication]. Per-request configuration lets
106106
callers change how documents are parsed, widening what anyone who can reach the server can do.
107107

108+
=== Presets — named configuration without `/config`
109+
110+
A *preset* is a named, vetted parse-context fragment: parser and component configurations keyed
111+
by friendly name, defined once in the server config (or shipped with Tika) and selected whole by
112+
name. Callers apply one by inserting `preset/{name}` directly after the resource root:
113+
`/tika/preset/{name}[/text|/html|/xml|/md|/json[/{handlerType}]]`,
114+
`/rmeta/preset/{name}[/{handlerType}]`, `/unpack/preset/{name}[/all]`.
115+
116+
[source,bash]
117+
----
118+
curl -T document.pdf http://localhost:9998/tika/preset/my-preset/text
119+
curl -T document.pdf http://localhost:9998/rmeta/preset/my-preset/text
120+
curl -T document.pdf http://localhost:9998/unpack/preset/my-preset
121+
----
122+
123+
Nothing is active unless the top-level `presets` key names it. `true` activates a preset from
124+
the classpath *catalog* (definitions shipped with Tika, so their content tracks the Tika
125+
version); an object defines a preset in place, replacing any same-named catalog definition
126+
wholesale; `false`/`null` is an explicit no-op. Catalog jars can never activate themselves —
127+
every active preset is a visible line in the operator's config, and `true` naming nothing in
128+
the catalog fails startup:
129+
130+
[source,json]
131+
----
132+
{
133+
"presets": {
134+
"some-catalog-preset": true,
135+
"no-ocr": { "pdf-parser": { "ocr": { "strategy": "NO_OCR" } } }
136+
}
137+
}
138+
----
139+
140+
Presets are deliberately narrow: a request selects exactly one, the preset routes take no
141+
`config` part, and a preset never combines with request-supplied configuration — a variant is
142+
another preset. Because preset content is operator- or Tika-vetted, the preset routes do *not*
143+
require `allowPerRequestConfig`, and the `preset/{name}` path segment gives network controls an
144+
addressable surface — a reverse proxy can allow `/rmeta/preset/` while blocking `/rmeta/config`
145+
entirely. An unknown preset name answers `404`. Names are letters, digits, `.`, `_`, `-`
146+
(max 100 chars) and may not start with `config` (that path fragment gates the `/config`
147+
endpoints).
148+
149+
Only the preset's *name* travels with a request: the forked parse worker resolves the content
150+
from its own copy of the server config, with the same trust as the config's own
151+
`parse-context` block. A preset can therefore bind components per-request `/config` input may
152+
not (detectors, embedded-document extraction, exception reporting, ...) and raise timeout
153+
limits above the per-request clamp. Every active preset resolves at startup, so a bad preset
154+
fails the server, not its first request.
155+
156+
Output format on the preset routes: an explicit format segment (`/tika/preset/{name}/text`)
157+
always wins; without one, a `ContentHandlerFactory` the preset binds decides, then the
158+
config's, then the endpoint default (Markdown).
159+
160+
On the batch surfaces, a `FetchEmitTuple` submitted to `/pipes` or `/async` may carry a
161+
top-level `"preset": "name"` field, resolved the same way beneath the tuple's own
162+
`parseContext`; an unknown name there answers the `PRESET_NOT_FOUND` status (HTTP `400`).
163+
108164
=== `allowPipes` — the `/pipes` and `/async` endpoints
109165

110166
`/pipes` and `/async` drive process-isolated batch parsing through your configured fetchers and

tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/FetchEmitTuple.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public enum ON_PARSE_EXCEPTION {
3838
private final Metadata metadata;
3939
private final ParseContext parseContext;
4040
private final ON_PARSE_EXCEPTION onParseException;
41+
// Preset selector only: the server resolves the content from its own config
42+
private final String presetName;
4143

4244
public FetchEmitTuple(String id, FetchKey fetchKey, EmitKey emitKey) {
4345
this(id, fetchKey, emitKey, new Metadata());
@@ -52,12 +54,19 @@ public FetchEmitTuple(String id, FetchKey fetchKey, EmitKey emitKey, Metadata me
5254

5355
public FetchEmitTuple(String id, FetchKey fetchKey, EmitKey emitKey, Metadata metadata, ParseContext parseContext,
5456
ON_PARSE_EXCEPTION onParseException) {
57+
this(id, fetchKey, emitKey, metadata, parseContext, onParseException, null);
58+
}
59+
60+
/** @since Apache Tika 4.1.0 */
61+
public FetchEmitTuple(String id, FetchKey fetchKey, EmitKey emitKey, Metadata metadata, ParseContext parseContext,
62+
ON_PARSE_EXCEPTION onParseException, String presetName) {
5563
this.id = id;
5664
this.fetchKey = fetchKey;
5765
this.emitKey = emitKey;
5866
this.metadata = metadata;
5967
this.parseContext = parseContext;
6068
this.onParseException = onParseException;
69+
this.presetName = presetName;
6170
}
6271

6372
public String getId() {
@@ -86,6 +95,15 @@ public ON_PARSE_EXCEPTION getOnParseException() {
8695
return onParseException;
8796
}
8897

98+
/**
99+
* The selected preset's name, or null for none.
100+
*
101+
* @since Apache Tika 4.1.0
102+
*/
103+
public String getPresetName() {
104+
return presetName;
105+
}
106+
89107
@Override
90108
public boolean equals(Object o) {
91109
if (this == o) {
@@ -98,7 +116,8 @@ public boolean equals(Object o) {
98116
FetchEmitTuple that = (FetchEmitTuple) o;
99117
return Objects.equals(id, that.id) && Objects.equals(fetchKey, that.fetchKey) && Objects.equals(emitKey, that.emitKey)
100118
&& Objects.equals(metadata, that.metadata) &&
101-
Objects.equals(parseContext, that.parseContext) && onParseException == that.onParseException;
119+
Objects.equals(parseContext, that.parseContext) && onParseException == that.onParseException &&
120+
Objects.equals(presetName, that.presetName);
102121
}
103122

104123
@Override
@@ -109,13 +128,14 @@ public int hashCode() {
109128
result = 31 * result + Objects.hashCode(metadata);
110129
result = 31 * result + Objects.hashCode(parseContext);
111130
result = 31 * result + Objects.hashCode(onParseException);
131+
result = 31 * result + Objects.hashCode(presetName);
112132
return result;
113133
}
114134

115135
@Override
116136
public String toString() {
117137
return "FetchEmitTuple{" + "id='" + id + '\'' + ", fetchKey=" + fetchKey + ", emitKey=" + emitKey +
118138
", metadata=" + metadata + ", parseContext=" + parseContext +
119-
", onParseException=" + onParseException + '}';
139+
", onParseException=" + onParseException + ", presetName='" + presetName + "'}";
120140
}
121141
}

tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/PipesResult.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public enum RESULT_STATUS {
6666
EMIT_EXCEPTION(CATEGORY.TASK_EXCEPTION),
6767
FETCHER_NOT_FOUND(CATEGORY.TASK_EXCEPTION),
6868
EMITTER_NOT_FOUND(CATEGORY.TASK_EXCEPTION),
69+
PRESET_NOT_FOUND(CATEGORY.TASK_EXCEPTION),
6970
PAYLOAD_LIMIT_EXCEEDED(CATEGORY.TASK_EXCEPTION),
7071

7172
// Process crashes - forked process died, auto-restart

tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.ID;
2727
import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.METADATA_KEY;
2828
import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.ON_PARSE_EXCEPTION;
29+
import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.PRESET;
2930
import static org.apache.tika.serialization.serdes.ParseContextSerializer.PARSE_CONTEXT;
3031

3132
import java.io.IOException;
@@ -40,6 +41,7 @@
4041
import com.fasterxml.jackson.databind.JsonDeserializer;
4142
import com.fasterxml.jackson.databind.JsonNode;
4243

44+
import org.apache.tika.config.loader.PresetRegistry;
4345
import org.apache.tika.metadata.Metadata;
4446
import org.apache.tika.parser.ParseContext;
4547
import org.apache.tika.pipes.api.ComponentIds;
@@ -55,7 +57,7 @@ public class FetchEmitTupleDeserializer extends JsonDeserializer<FetchEmitTuple>
5557

5658
private static final Set<String> KNOWN_KEYS = Set.of(
5759
ID, FETCHER, FETCH_KEY, EMITTER, EMIT_KEY, FETCH_RANGE_START, FETCH_RANGE_END,
58-
METADATA_KEY, PARSE_CONTEXT, ON_PARSE_EXCEPTION);
60+
METADATA_KEY, PARSE_CONTEXT, ON_PARSE_EXCEPTION, PRESET);
5961

6062
private final boolean restricted;
6163

@@ -114,10 +116,11 @@ public FetchEmitTuple deserialize(JsonParser jsonParser, DeserializationContext
114116
ParseContext parseContext = parseContextNode == null ? new ParseContext()
115117
: ParseContextDeserializer.readParseContext(parseContextNode, true);
116118
FetchEmitTuple.ON_PARSE_EXCEPTION onParseException = readOnParseException(root);
119+
String presetName = readPresetName(root);
117120

118121
return new FetchEmitTuple(id, new FetchKey(fetcherId, fetchKey, fetchRangeStart, fetchRangeEnd),
119122
new EmitKey(emitterName, emitKey), metadata, parseContext,
120-
onParseException);
123+
onParseException, presetName);
121124
}
122125

123126
/**
@@ -147,6 +150,16 @@ private static void rejectUnknownKeys(JsonNode root) throws IOException {
147150
}
148151
}
149152

153+
// A preset name is only a selector; the shared syntax rule also bounds its length.
154+
private static String readPresetName(JsonNode root) throws IOException {
155+
String presetName = readVal(PRESET, root, null, false);
156+
if (presetName != null && !PresetRegistry.isValidName(presetName)) {
157+
throw new IOException("invalid preset name (letters, digits, '.', '_', '-'; " +
158+
"max 100 chars; may not start with 'config')");
159+
}
160+
return presetName;
161+
}
162+
150163
private static FetchEmitTuple.ON_PARSE_EXCEPTION readOnParseException(JsonNode root) throws IOException {
151164
JsonNode onParseExNode = root.get(ON_PARSE_EXCEPTION);
152165
if (onParseExNode == null) {

tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleSerializer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class FetchEmitTupleSerializer extends JsonSerializer<FetchEmitTuple> {
4040
public static final String EMIT_KEY = "emitKey";
4141
public static final String METADATA_KEY = "metadata";
4242
public static final String ON_PARSE_EXCEPTION = "onParseException";
43+
public static final String PRESET = "preset";
4344

4445
public void serialize(FetchEmitTuple t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
4546

@@ -59,6 +60,9 @@ public void serialize(FetchEmitTuple t, JsonGenerator jsonGenerator, SerializerP
5960
jsonGenerator.writeObjectField(METADATA_KEY, t.getMetadata());
6061
}
6162
jsonGenerator.writeStringField(ON_PARSE_EXCEPTION, t.getOnParseException().name().toLowerCase(Locale.US));
63+
if (t.getPresetName() != null) {
64+
jsonGenerator.writeStringField(PRESET, t.getPresetName());
65+
}
6266
ParseContext parseContext = t.getParseContext();
6367
// Tailored: ParseContextSerializer's generic refusal suggests registering the
6468
// component -- for InlineBytes, exactly the forbidden fix.

tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/PipesRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static PipesRequest of(FetchEmitTuple t) {
6060
copy.copyFrom(ctx);
6161
copy.set(InlineBytes.class, null);
6262
FetchEmitTuple stripped = new FetchEmitTuple(t.getId(), t.getFetchKey(), t.getEmitKey(),
63-
t.getMetadata(), copy, t.getOnParseException());
63+
t.getMetadata(), copy, t.getOnParseException(), t.getPresetName());
6464
return new PipesRequest(stripped, inline.getBytes());
6565
}
6666

tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ConnectionHandler.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ private void mainLoop() {
216216
ParseContext mergedContext = null;
217217
try {
218218
long ctxStart = System.nanoTime();
219-
mergedContext = resources.createMergedParseContext(fetchEmitTuple.getParseContext());
219+
mergedContext = resources.createMergedParseContext(
220+
fetchEmitTuple.getParseContext(), fetchEmitTuple.getPresetName());
220221
ParseContextUtils.resolveAll(mergedContext, getClass().getClassLoader());
221222
ServerProtocolIO.validateParseContext(mergedContext);
222223
ServerProtocolIO.clampRequestTimeoutLimits(
@@ -239,6 +240,11 @@ private void mainLoop() {
239240

240241
loopUntilDone(fetchEmitTuple, mergedContext, intermediateResult, countDownLatch, parseTimeout);
241242
logTiming(fetchEmitTuple.getId());
243+
} catch (PresetNotFoundException e) {
244+
// caller error, not a server fault: answer it and keep serving
245+
LOG.warn("handlerId={}: id={}: {}", handlerId, fetchEmitTuple.getId(), e.getMessage());
246+
protocolIO.writeFinished(new PipesResult(
247+
PipesResult.RESULT_STATUS.PRESET_NOT_FOUND, e.getMessage()));
242248
} catch (TikaConfigException e) {
243249
LOG.error("handlerId={}: config error processing request", handlerId, e);
244250
handleCrash(PipesMessageType.UNSPECIFIED_CRASH, fetchEmitTuple.getId(), e);

0 commit comments

Comments
 (0)