Skip to content

Commit 8fced66

Browse files
authored
TIKA-4819: Embedded extractor stateless (#3021)
1 parent 59e6751 commit 8fced66

79 files changed

Lines changed: 625 additions & 449 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGES.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ Release 4.0.0 - ???
3030
API cleanups (cf: prefix, writelimiter package, smaller removals) are
3131
enumerated in the migration guide, migrating-to-4x.adoc (TIKA-4816).
3232

33+
* Parsing with a concrete parser (not AutoDetectParser) and an empty
34+
ParseContext no longer auto-generates an AutoDetectParser to handle
35+
embedded files. Users must set the Parser of their choice for embedded
36+
files. EmbeddedDocumentExtractorFactory and friends are removed;
37+
ParsingEmbeddedDocumentExtractor/UnpackExtractor are now stateless
38+
singletons (TIKA-4819).
39+
40+
* Parsing with a concrete parser (not AutoDetectParser) and an empty
41+
ParseContext no longer auto-generates a Detector to identify embedded
42+
files; they are reported as application/octet-stream instead. Users
43+
must set the Detector of their choice for embedded files (TIKA-4819).
44+
3345
* tika-server: the /translate endpoints have been removed (TIKA-4809).
3446

3547
* tika-server: /detect/stream and /language/stream and /language/string have

docs/modules/ROOT/pages/advanced/setting-limits.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,8 @@ malicious files that may expand to enormous sizes when unpacked.
342342
|`maxUnpackBytes`
343343
|10 GB
344344
|Maximum total bytes to extract from all embedded documents per file. Set to -1 for
345-
unlimited (not recommended for untrusted input).
345+
unlimited (not recommended for untrusted input). 0 is not unlimited -- it means zero
346+
bytes.
346347
|===
347348

348349
=== Behavior

docs/modules/ROOT/pages/developers/serialization.adoc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ one of these known interfaces:
130130
* `Parser`, `Detector`, `EncodingDetector`
131131
* `MetadataFilter`, `Translator`, `Renderer`
132132
* `DigesterFactory`, `ContentHandlerFactory`
133-
* `EmbeddedDocumentExtractorFactory`, `MetadataWriteLimiterFactory`
133+
* `MetadataWriteLimiterFactory`, `EmbeddedDocumentExtractor`
134134

135135
[source,java]
136136
----
@@ -175,7 +175,6 @@ Components not implementing first-class interfaces go to `parse-context.idx`:
175175

176176
* `DigesterFactory` - Digest/checksum calculation
177177
* `ContentHandlerFactory` - SAX content handler creation
178-
* `EmbeddedDocumentExtractorFactory` - Embedded document handling
179178
* `MetadataWriteLimiterFactory` - Metadata write limiting
180179

181180
== Self-Configuring Components
@@ -284,8 +283,8 @@ fail-closed gate on top of the registry:
284283
`ContentHandlerDecoratorFactory`, `DigesterFactory`,
285284
`MetadataWriteLimiterFactory`, `UnpackSelector`
286285
* Types with exec/IO/network capability or control over which components run
287-
are blocked: `Parser`, `Detector`, `EncodingDetector`, `Renderer`,
288-
`Translator`, `EmbeddedDocumentExtractorFactory`
286+
are blocked: `Parser`, `Detector`, `EncodingDetector`, `Renderer`, `Translator`,
287+
`EmbeddedDocumentExtractor`
289288
* The check is fail-closed: a newly added context-key interface is blocked until
290289
it is consciously allow-listed
291290
* The whole tree is scanned *before* any component is constructed

docs/modules/ROOT/pages/migration-to-4x/migrating-to-4x.adoc

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,121 @@ External parsers must now be explicitly configured via JSON. See
242242
xref:configuration/parsers/external-parser.adoc[External Parser Configuration]
243243
for details.
244244

245+
=== EmbeddedDocumentExtractor is now stateless
246+
247+
[WARNING]
248+
====
249+
If you call a concrete parser directly instead of going through `AutoDetectParser`, you
250+
must set `Parser.class` in the `ParseContext` yourself or embedded documents are
251+
**silently skipped** -- no content, no exception. The same applies to `Detector.class`:
252+
without it, embedded documents are reported as `application/octet-stream` instead of
253+
being identified. See <<bare-context-embedded-skip,Behavior change>> below.
254+
====
255+
256+
`ParsingEmbeddedDocumentExtractor` (and Tika Pipes' `UnpackExtractor`) no longer capture a
257+
`ParseContext` at construction. Every method now takes the `ParseContext` of the enclosing
258+
parse as a parameter, and a single shared instance is reused across parses instead of one
259+
being built per parse.
260+
261+
**3.x / early 4.x:**
262+
[source,java]
263+
----
264+
EmbeddedDocumentExtractor extractor = new ParsingEmbeddedDocumentExtractor(context);
265+
if (extractor.shouldParseEmbedded(metadata)) {
266+
extractor.parseEmbedded(tis, handler, metadata, outputHtml);
267+
}
268+
----
269+
270+
**4.x:**
271+
[source,java]
272+
----
273+
EmbeddedDocumentExtractor extractor = ParsingEmbeddedDocumentExtractor.INSTANCE;
274+
if (extractor.shouldParseEmbedded(metadata, context)) {
275+
extractor.parseEmbedded(tis, handler, metadata, context, outputHtml);
276+
}
277+
----
278+
279+
This affects:
280+
281+
* `EmbeddedDocumentExtractor#shouldParseEmbedded(Metadata)` -> `shouldParseEmbedded(Metadata, ParseContext)`.
282+
Any custom `EmbeddedDocumentExtractor` implementation must add the parameter.
283+
* `ParsingEmbeddedDocumentExtractor`'s public constructor is gone -- use the
284+
`ParsingEmbeddedDocumentExtractor.INSTANCE` singleton (or `UnpackExtractor.INSTANCE` in
285+
Tika Pipes). A subclass that called `super(context)` should drop the constructor and read
286+
`ParseContext` from the method parameter instead of a captured field.
287+
* `ParsingEmbeddedDocumentExtractor#checkEmbeddedLimits(ParseRecord)` ->
288+
`checkEmbeddedLimits(ParseRecord, ParseContext)`, and `isWriteFileNameToContent()` ->
289+
`isWriteFileNameToContent(ParseContext)`. A subclass overriding either must add the
290+
parameter -- without `@Override`, the old signature silently becomes a dead, unused overload
291+
instead of a compile error.
292+
* `EmbeddedDocumentExtractorFactory`, `EmbeddedDocumentByteStoreExtractorFactory`,
293+
`StandardExtractorFactory`, and Tika Pipes' `UnpackExtractorFactory` are deleted -- there is
294+
no longer a per-parse object to build. Code that supplied a custom factory should instead
295+
bind an `EmbeddedDocumentExtractor` instance directly:
296+
+
297+
[source,java]
298+
----
299+
// 3.x/early 4.x
300+
context.set(EmbeddedDocumentExtractorFactory.class, new MyExtractorFactory());
301+
302+
// 4.x
303+
context.set(EmbeddedDocumentExtractor.class, MyExtractor.INSTANCE);
304+
----
305+
* `EmbeddedDocumentUtil`'s instance API is removed (the constructor, and the instance methods
306+
`getPasswordProvider()`, `getDetector()`, `getMimeTypes()`, `getExtension(TikaInputStream,
307+
Metadata)`, `shouldParseEmbedded(Metadata)`, `parseEmbedded(...)`). Use the static
308+
replacements, which take `ParseContext` explicitly: `EmbeddedDocumentUtil.getDetector(context)`,
309+
`EmbeddedDocumentUtil.getMimeTypes(context)`,
310+
`EmbeddedDocumentUtil.getExtension(tis, metadata, context)`, or call the methods on the
311+
`EmbeddedDocumentExtractor` obtained from `EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context)`
312+
directly. `getPasswordProvider()` had no replacement added since it had no callers;
313+
use `context.get(PasswordProvider.class)`.
314+
315+
[[bare-context-embedded-skip]]
316+
==== Behavior change: parsing a concrete parser directly with a bare `ParseContext`
317+
318+
[WARNING]
319+
====
320+
Calling a concrete parser directly (bypassing `AutoDetectParser`) with a `ParseContext` that
321+
has no `Parser.class` set now **silently skips embedded documents** instead of constructing an
322+
SPI-discovered `AutoDetectParser` to parse them:
323+
324+
[source,java]
325+
----
326+
new PDFParser().parse(tis, handler, metadata, new ParseContext());
327+
// 3.x/early 4.x: embedded documents parsed by an SPI-discovered AutoDetectParser
328+
// (bypassing whatever parser/limits/selectors the caller actually configured)
329+
// 4.x: embedded documents are skipped -- no content, no exception
330+
----
331+
332+
If you rely on embedded documents being parsed, set `Parser.class` in the `ParseContext`
333+
(typically to an `AutoDetectParser`) before calling a concrete parser directly, or go through
334+
`AutoDetectParser` in the first place, which does this for you automatically.
335+
====
336+
337+
[[bare-context-detector-noop]]
338+
==== Behavior change: identifying embedded files with a bare `ParseContext`
339+
340+
[WARNING]
341+
====
342+
Some container parsers (`OpenDocumentParser`, the POIFS-based Office parsers, `RFC822Parser`)
343+
detect each embedded file's media type to label it in the output metadata. With a bare
344+
`ParseContext` (no `Detector.class` set), this now reports every embedded file as
345+
`application/octet-stream` instead of constructing an SPI-discovered `DefaultDetector`:
346+
347+
[source,java]
348+
----
349+
new OpenDocumentParser().parse(tis, handler, metadata, new ParseContext());
350+
// 3.x/early 4.x: embedded pictures identified by an SPI-discovered DefaultDetector
351+
// (e.g. image/jpeg, image/png)
352+
// 4.x: embedded pictures are all labeled application/octet-stream
353+
----
354+
355+
If you rely on embedded files being identified, set `Detector.class` in the `ParseContext`
356+
before calling a concrete parser directly, or go through `AutoDetectParser` in the first
357+
place, which does this for you automatically.
358+
====
359+
245360
== Timeout Model Changes
246361

247362
4.x replaces the previous ad hoc, per-parser timeout handling with a single unified model
@@ -286,3 +401,11 @@ the full list of behavioral changes and required config edits.
286401
URL, XPATH}` -- dead enum constants, never produced by any `Property` factory
287402
* `Property.internalClosedChoise`/`internalOpenChoise`/`externalClosedChoise`/`externalOpenChoise`
288403
-- renamed to `...Choice` (typo-fix rename, no forwarders)
404+
* `EmbeddedDocumentExtractorFactory`, `EmbeddedDocumentByteStoreExtractorFactory`,
405+
`StandardExtractorFactory`, `UnpackExtractorFactory` -- deleted; bind an
406+
`EmbeddedDocumentExtractor` instance directly instead (see
407+
<<EmbeddedDocumentExtractor is now stateless>> above)
408+
* `ParsingEmbeddedDocumentExtractor(ParseContext)` constructor -- removed; use the
409+
`ParsingEmbeddedDocumentExtractor.INSTANCE` singleton
410+
* `EmbeddedDocumentUtil`'s instance API (constructor and all instance methods) -- removed;
411+
use the static equivalents, which now take `ParseContext` explicitly

docs/modules/ROOT/pages/pipes/unpack-config.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ All options below are fields of the `unpack-config` block — nest them inside
8383
|`maxUnpackBytes`
8484
|long
8585
|10GB
86-
|Maximum total bytes to extract per file. Set to `-1` for unlimited (not recommended).
86+
|Maximum total bytes to extract per file. Set to `-1` for unlimited (not recommended). `0`
87+
is not unlimited -- it means zero bytes, so the first embedded file's extraction is
88+
immediately capped.
8789

8890
|`includeOriginal`
8991
|boolean
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.detect;
18+
19+
import java.io.IOException;
20+
21+
import org.apache.tika.annotation.TikaComponent;
22+
import org.apache.tika.io.TikaInputStream;
23+
import org.apache.tika.metadata.Metadata;
24+
import org.apache.tika.mime.MediaType;
25+
import org.apache.tika.parser.ParseContext;
26+
27+
/**
28+
* Dummy detector that always reports {@code application/octet-stream} without even
29+
* reading the given document stream. Useful as a sentinel default: reporting an honest
30+
* "unknown" is preferable to a partially-informed guess from an unconfigured detector.
31+
*/
32+
@TikaComponent(spi = false)
33+
public class NoOpDetector implements Detector {
34+
35+
public static final NoOpDetector INSTANCE = new NoOpDetector();
36+
37+
private static final long serialVersionUID = 1L;
38+
39+
@Override
40+
public MediaType detect(TikaInputStream tis, Metadata metadata, ParseContext parseContext)
41+
throws IOException {
42+
// Honor the Detector contract (mark before reading, reset before returning) even though
43+
// nothing is read: callers may rely on the mark still being valid for their own reset().
44+
if (tis != null) {
45+
tis.mark(1);
46+
tis.reset();
47+
}
48+
return MediaType.OCTET_STREAM;
49+
}
50+
}

tika-core/src/main/java/org/apache/tika/extractor/EmbeddedDocumentByteStoreExtractorFactory.java

Lines changed: 0 additions & 32 deletions
This file was deleted.

tika-core/src/main/java/org/apache/tika/extractor/EmbeddedDocumentExtractor.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,23 @@
2525
import org.apache.tika.metadata.Metadata;
2626
import org.apache.tika.parser.ParseContext;
2727

28+
/**
29+
* Implementations are typically bound as a single shared instance reused across concurrent
30+
* parses (see {@link ParsingEmbeddedDocumentExtractor#INSTANCE}) -- do not cache per-parse
31+
* state on {@code this}; read it from the {@link ParseContext} parameter on each call instead.
32+
*/
2833
public interface EmbeddedDocumentExtractor {
2934
/**
3035
* Determines whether the given embedded document should be parsed.
3136
* <p>
3237
* Note: Implementations may throw {@link org.apache.tika.exception.EmbeddedLimitReachedException}
3338
* (a RuntimeException) if a limit is exceeded and throwing is configured.
3439
*
35-
* @param metadata the metadata for the embedded document
40+
* @param metadata the metadata for the embedded document
41+
* @param parseContext the parse context of the enclosing parse
3642
* @return true if the embedded document should be parsed
3743
*/
38-
boolean shouldParseEmbedded(Metadata metadata);
44+
boolean shouldParseEmbedded(Metadata metadata, ParseContext parseContext);
3945

4046
/**
4147
* Processes the supplied embedded resource, calling the delegating

tika-core/src/main/java/org/apache/tika/extractor/EmbeddedDocumentExtractorFactory.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)