You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tika.apache.org by ta...@apache.org on 2025/12/21 04:29:40 UTC
(tika) branch TIKA-4585-simplify-serialization created (now 05e3fc4d9)
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a change to branch TIKA-4585-simplify-serialization
in repository https://gitbox.apache.org/repos/asf/tika.git
at 05e3fc4d9 TIKA-4585 -- further progress
This branch includes the following new commits:
new 5de26e049 TIKA-4585 -- simplify ParseContext serialization -WIP
new 47df95adc TIKA-4585 -- further progress - WIP
new fb4cb1c5a TIKA-4585 -- further progress - WIP
new ff1281119 TIKA-4585 -- further progress - WIP
new 05e3fc4d9 TIKA-4585 -- further progress
The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(tika) 04/05: TIKA-4585 -- further progress - WIP
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4585-simplify-serialization
in repository https://gitbox.apache.org/repos/asf/tika.git
commit ff1281119dc3b7d4abb5fed2191074a7f9e1ec35
Author: tallison <ta...@apache.org>
AuthorDate: Sat Dec 20 22:45:19 2025 -0500
TIKA-4585 -- further progress - WIP
---
.../apache/tika/parser/geo/topic/GeoParser.java | 14 +++
.../apache/tika/config/TikaDetectorConfigTest.java | 42 +++++----
.../DoublingContentHandlerDecoratorFactory.java | 2 +
.../configs/TIKA-1702-detector-exclude.json | 2 +-
.../configs/TIKA-1708-detector-default.json | 2 +-
...2273-encoding-detector-outside-static-init.json | 2 +-
...IKA-2273-exclude-encoding-detector-default.json | 2 +-
.../TIKA-2273-no-icu4j-encoding-detector.json | 2 +-
.../configs/test-default-with-exclusions.json | 2 +-
.../test/resources/configs/tika-4424-config.json | 2 +-
.../configs/tika-config-digests-pdf-only.json | 2 +-
...a-config-doubling-custom-handler-decorator.json | 4 +-
.../resources/configs/tika-config-lib-pst.json | 2 +-
.../resources/configs/tika-config-no-names.json | 5 +-
...a-config-upcasing-custom-handler-decorator.json | 54 +++++------
.../resources/configs/tika-config-with-names.json | 5 +-
.../configs/tika-config-write-filter.json | 11 +--
.../test/resources/configs/tika-unrar-config.json | 2 +-
.../org/apache/tika/config/TIKA-1558-exclude.json | 10 +--
.../tika/config/TIKA-1702-translator-default.json | 6 +-
.../config/TIKA-1702-translator-empty-default.json | 6 +-
.../tika/config/TIKA-1702-translator-empty.json | 6 +-
.../resources/configs/tika-config-truncate.json | 7 +-
.../org/apache/tika/config/loader/TikaLoader.java | 100 ++++++++++++++++++---
.../org/apache/tika/serialization/TikaModule.java | 6 ++
.../serdes/ParseContextDeserializer.java | 33 +++++--
.../serdes/ParseContextSerializer.java | 28 ++++--
27 files changed, 244 insertions(+), 115 deletions(-)
diff --git a/tika-parsers/tika-parsers-ml/tika-parser-nlp-module/src/main/java/org/apache/tika/parser/geo/topic/GeoParser.java b/tika-parsers/tika-parsers-ml/tika-parser-nlp-module/src/main/java/org/apache/tika/parser/geo/topic/GeoParser.java
index a797601e2..6208ade3b 100644
--- a/tika-parsers/tika-parsers-ml/tika-parser-nlp-module/src/main/java/org/apache/tika/parser/geo/topic/GeoParser.java
+++ b/tika-parsers/tika-parsers-ml/tika-parser-nlp-module/src/main/java/org/apache/tika/parser/geo/topic/GeoParser.java
@@ -33,6 +33,8 @@ import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.TikaComponent;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
@@ -58,6 +60,18 @@ public class GeoParser implements Parser {
private NameFinderME nameFinder;
private boolean available;
+ public GeoParser() {
+ // Default constructor - uses default GeoParserConfig
+ }
+
+ public GeoParser(GeoParserConfig config) {
+ this.defaultConfig = config;
+ }
+
+ public GeoParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig, GeoParserConfig.class));
+ }
+
@Override
public Set<MediaType> getSupportedTypes(ParseContext parseContext) {
return SUPPORTED_TYPES;
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/TikaDetectorConfigTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/TikaDetectorConfigTest.java
index 60efea055..8123b397f 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/TikaDetectorConfigTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/config/TikaDetectorConfigTest.java
@@ -113,29 +113,33 @@ public class TikaDetectorConfigTest extends TikaTest {
private void assertDetectors(CompositeDetector detector, boolean shouldHavePOIFS,
boolean shouldHaveZip) {
- boolean hasZip = false;
- boolean hasPOIFS = false;
- for (Detector d : detector.getDetectors()) {
- if (d instanceof DefaultZipContainerDetector) {
- if (shouldHaveZip) {
- hasZip = true;
- } else {
- fail("Shouldn't have the ZipContainerDetector from config");
- }
- }
- if (d instanceof POIFSContainerDetector) {
- if (shouldHavePOIFS) {
- hasPOIFS = true;
- } else {
- fail("Shouldn't have the POIFSContainerDetector from config");
- }
- }
+ boolean hasZip = hasDetectorRecursively(detector, DefaultZipContainerDetector.class);
+ boolean hasPOIFS = hasDetectorRecursively(detector, POIFSContainerDetector.class);
+
+ if (shouldHaveZip) {
+ assertTrue(hasZip, "Should have the ZipContainerDetector");
+ } else if (hasZip) {
+ fail("Shouldn't have the ZipContainerDetector from config");
}
+
if (shouldHavePOIFS) {
assertTrue(hasPOIFS, "Should have the POIFSContainerDetector");
+ } else if (hasPOIFS) {
+ fail("Shouldn't have the POIFSContainerDetector from config");
}
- if (shouldHaveZip) {
- assertTrue(hasZip, "Should have the ZipContainerDetector");
+ }
+
+ private boolean hasDetectorRecursively(Detector detector, Class<? extends Detector> targetClass) {
+ if (targetClass.isInstance(detector)) {
+ return true;
+ }
+ if (detector instanceof CompositeDetector) {
+ for (Detector child : ((CompositeDetector) detector).getDetectors()) {
+ if (hasDetectorRecursively(child, targetClass)) {
+ return true;
+ }
+ }
}
+ return false;
}
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/sax/DoublingContentHandlerDecoratorFactory.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/sax/DoublingContentHandlerDecoratorFactory.java
index 60aa7c7d2..1cd2da3c1 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/sax/DoublingContentHandlerDecoratorFactory.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/sax/DoublingContentHandlerDecoratorFactory.java
@@ -21,9 +21,11 @@ import java.util.Locale;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
+import org.apache.tika.config.TikaComponent;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
+@TikaComponent
public class DoublingContentHandlerDecoratorFactory implements ContentHandlerDecoratorFactory {
private static final char[] NEWLINE = new char[]{'\n'};
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1702-detector-exclude.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1702-detector-exclude.json
index 80a611f6b..fe356421d 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1702-detector-exclude.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1702-detector-exclude.json
@@ -3,7 +3,7 @@
"detectors": [
{
"default-detector": {
- "_exclude": [
+ "exclude": [
"default-zip-container-detector",
"poifs-container-detector"
]
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1708-detector-default.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1708-detector-default.json
index 4c49c1e46..2c2e0e676 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1708-detector-default.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-1708-detector-default.json
@@ -3,7 +3,7 @@
"detectors": [
{
"default-detector" : {
- "_exclude": [
+ "exclude": [
"default-zip-container-detector"
]
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-encoding-detector-outside-static-init.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-encoding-detector-outside-static-init.json
index c1818466e..2c05becdc 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-encoding-detector-outside-static-init.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-encoding-detector-outside-static-init.json
@@ -10,7 +10,7 @@
"encoding-detectors": [
{
"default-encoding-detector" : {
- "_exclude":["icu4j-encoding-detector"]
+ "exclude":["icu4j-encoding-detector"]
}
}
]
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-exclude-encoding-detector-default.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-exclude-encoding-detector-default.json
index 56327103c..240924a28 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-exclude-encoding-detector-default.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-exclude-encoding-detector-default.json
@@ -3,7 +3,7 @@
"encoding-detectors": [
{
"default-encoding-detector": {
- "_exclude": [
+ "exclude": [
"html-encoding-detector"
]
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-no-icu4j-encoding-detector.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-no-icu4j-encoding-detector.json
index b37a45121..809932615 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-no-icu4j-encoding-detector.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/TIKA-2273-no-icu4j-encoding-detector.json
@@ -2,7 +2,7 @@
"encoding-detectors": [
{
"default-encoding-detector":{
- "_exclude": [
+ "exclude": [
"icu4j-encoding-detector"
]
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/test-default-with-exclusions.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/test-default-with-exclusions.json
index 5233e290f..c29e0f420 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/test-default-with-exclusions.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/test-default-with-exclusions.json
@@ -2,7 +2,7 @@
"parsers": [
{
"default-parser": {
- "_exclude": ["pdf-parser", "jsoup-parser"]
+ "exclude": ["pdf-parser", "jsoup-parser"]
}
}
]
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-4424-config.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-4424-config.json
index 82a03978b..ceea6d09d 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-4424-config.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-4424-config.json
@@ -2,7 +2,7 @@
"detectors": [
{
"default-detector": {
- "_exclude": ["default-zip-container-detector"]
+ "exclude": ["default-zip-container-detector"]
}
}
]
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-digests-pdf-only.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-digests-pdf-only.json
index c23c2eb1f..124b07adc 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-digests-pdf-only.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-digests-pdf-only.json
@@ -2,7 +2,7 @@
"parsers": [
{
"default-parser": {
- "_exclude": [
+ "exclude": [
"pdf-parser"
]
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-doubling-custom-handler-decorator.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-doubling-custom-handler-decorator.json
index dcd44b7f3..012142231 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-doubling-custom-handler-decorator.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-doubling-custom-handler-decorator.json
@@ -2,8 +2,6 @@
"auto-detect-parser": {
"spoolToDisk": 1000,
"outputThreshold": 1000,
- "contentHandlerDecoratorFactory": {
- "@class": "org.apache.tika.sax.DoublingContentHandlerDecoratorFactory"
- }
+ "contentHandlerDecoratorFactory": "doubling-content-handler-decorator-factory"
}
}
\ No newline at end of file
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-lib-pst.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-lib-pst.json
index da45f42ee..1396afc7a 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-lib-pst.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-lib-pst.json
@@ -2,7 +2,7 @@
"parsers": [
{
"default-parser": {
- "_exclude": [
+ "exclude": [
"outlook-pst-parser",
"pst-mail-item-parser"
]
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-no-names.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-no-names.json
index 5938163df..2f0ac2a2f 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-no-names.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-no-names.json
@@ -3,8 +3,9 @@
"spoolToDisk": 123450,
"outputThreshold": 678900,
"embeddedDocumentExtractorFactory": {
- "@class": "org.apache.tika.extractor.RUnpackExtractorFactory",
- "writeFileNameToContent": false
+ "runpack-extractor-factory": {
+ "writeFileNameToContent": false
+ }
}
}
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-upcasing-custom-handler-decorator.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-upcasing-custom-handler-decorator.json
index bc2ce44da..a58fa91fc 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-upcasing-custom-handler-decorator.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-upcasing-custom-handler-decorator.json
@@ -6,36 +6,36 @@
"maximumDepth": 1000,
"maximumPackageEntryDepth": 1000,
"metadataWriteFilterFactory": {
- "@class": "org.apache.tika.metadata.writefilter.StandardWriteFilterFactory",
- "includeFields": [
- ],
- "excludeFields": [
- ],
- "maxKeySize": 1024,
- "maxFieldSize": 102400,
- "maxTotalEstimatedBytes": 10485760,
- "maxValuesPerField": 10,
- "includeEmpty": false
+ "standard-write-filter-factory": {
+ "includeFields": [
+ ],
+ "excludeFields": [
+ ],
+ "maxKeySize": 1024,
+ "maxFieldSize": 102400,
+ "maxTotalEstimatedBytes": 10485760,
+ "maxValuesPerField": 10,
+ "includeEmpty": false
+ }
},
"embeddedDocumentExtractorFactory": {
- "@class": "org.apache.tika.extractor.RUnpackExtractorFactory",
- "writeFileNameToContent": true,
- "embeddedBytesIncludeMimeTypes": [
- "text/pdf"
- ],
- "embeddedBytesExcludeMimeTypes": [
- "rtf/application"
- ],
- "embeddedBytesIncludeEmbeddedResourceTypes": [
- "appended"
- ],
- "embeddedBytesExcludeEmbeddedResourceTypes": [
- ],
- "maxEmbeddedBytesForExtraction": 10737418240
- },
- "contentHandlerDecoratorFactory": {
- "@class": "org.apache.tika.sax.UpcasingContentHandlerDecoratorFactory"
+ "runpack-extractor-factory": {
+ "writeFileNameToContent": true,
+ "embeddedBytesIncludeMimeTypes": [
+ "text/pdf"
+ ],
+ "embeddedBytesExcludeMimeTypes": [
+ "rtf/application"
+ ],
+ "embeddedBytesIncludeEmbeddedResourceTypes": [
+ "appended"
+ ],
+ "embeddedBytesExcludeEmbeddedResourceTypes": [
+ ],
+ "maxEmbeddedBytesForExtraction": 10737418240
+ }
},
+ "contentHandlerDecoratorFactory": "upcasing-content-handler-decorator-factory",
"skipContainerDocumentDigest": false,
"digesterFactory": {
"commons-digester-factory": {}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-with-names.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-with-names.json
index ea1519ec0..0659adb85 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-with-names.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-with-names.json
@@ -3,8 +3,9 @@
"spoolToDisk": 123450,
"outputThreshold": 678900,
"embeddedDocumentExtractorFactory": {
- "@class": "org.apache.tika.extractor.RUnpackExtractorFactory",
- "writeFileNameToContent": true
+ "runpack-extractor-factory": {
+ "writeFileNameToContent": true
+ }
}
}
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-write-filter.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-write-filter.json
index 39141be2c..1b6f13c1c 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-write-filter.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-config-write-filter.json
@@ -13,11 +13,12 @@
}
},
"metadataWriteFilterFactory": {
- "@class": "org.apache.tika.metadata.writefilter.StandardWriteFilterFactory",
- "includeFields": [
- "X-TIKA-CONTENT",
- "dc:creator"
- ]
+ "standard-write-filter-factory": {
+ "includeFields": [
+ "X-TIKA-CONTENT",
+ "dc:creator"
+ ]
+ }
},
"throwOnZeroBytes": false
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-unrar-config.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-unrar-config.json
index de3fd5b32..5511b90b7 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-unrar-config.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/configs/tika-unrar-config.json
@@ -2,7 +2,7 @@
"parsers": [
{
"default-parser": {
- "_exclude": ["rar-parser"]
+ "exclude": ["rar-parser"]
}
},
{
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1558-exclude.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1558-exclude.json
index d25f49d85..72c9653da 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1558-exclude.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1558-exclude.json
@@ -1,14 +1,14 @@
{
"parsers": [
{
- "default-parser": {
- "_exclude": ["executable-parser"],
- "_mime-exclude": ["image/jpeg", "application/pdf"]
+ "empty-parser": {
+ "_mime-include": ["application/pdf"]
}
},
{
- "empty-parser": {
- "_mime-include": ["application/pdf"]
+ "default-parser": {
+ "exclude": ["executable-parser"],
+ "_mime-exclude": ["image/jpeg", "application/pdf"]
}
}
]
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-default.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-default.json
index aa268b6c4..a6f122528 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-default.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-default.json
@@ -1,5 +1,5 @@
{
- "translator": {
- "default-translator": {}
- }
+ "translator": [
+ "default-translator"
+ ]
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty-default.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty-default.json
index 73ad08c22..4439a14ba 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty-default.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty-default.json
@@ -1,5 +1,5 @@
{
- "translator": {
- "empty-translator": {}
- }
+ "translator": [
+ "empty-translator"
+ ]
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty.json
index 73ad08c22..4439a14ba 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty.json
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/resources/org/apache/tika/config/TIKA-1702-translator-empty.json
@@ -1,5 +1,5 @@
{
- "translator": {
- "empty-translator": {}
- }
+ "translator": [
+ "empty-translator"
+ ]
}
diff --git a/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-truncate.json b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-truncate.json
index fd8592020..873ce685a 100644
--- a/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-truncate.json
+++ b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-truncate.json
@@ -56,9 +56,10 @@
"mock-digester-factory": {}
},
"embeddedDocumentExtractorFactory": {
- "@class": "org.apache.tika.extractor.RUnpackExtractorFactory",
- "writeFileNameToContent": false,
- "maxEmbeddedBytesForExtraction": 10
+ "runpack-extractor-factory": {
+ "writeFileNameToContent": false,
+ "maxEmbeddedBytesForExtraction": 10
+ }
},
"throwOnZeroBytes": false
},
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
index 42f9b5992..115480d6e 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
@@ -35,15 +35,18 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.tika.config.GlobalSettings;
import org.apache.tika.config.ServiceLoader;
import org.apache.tika.detect.CompositeDetector;
+import org.apache.tika.detect.CompositeEncodingDetector;
import org.apache.tika.detect.DefaultDetector;
import org.apache.tika.detect.DefaultEncodingDetector;
import org.apache.tika.detect.Detector;
import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.language.translate.DefaultTranslator;
import org.apache.tika.language.translate.Translator;
import org.apache.tika.metadata.filter.CompositeMetadataFilter;
import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.metadata.filter.NoOpFilter;
+import org.apache.tika.mime.MediaType;
import org.apache.tika.mime.MediaTypeRegistry;
import org.apache.tika.mime.MimeTypes;
import org.apache.tika.parser.AbstractEncodingDetectorParser;
@@ -114,9 +117,17 @@ public class TikaLoader {
ComponentConfig.builder("encoding-detectors", EncodingDetector.class)
.loadAsList()
- .wrapWith(list -> list.isEmpty()
- ? new DefaultEncodingDetector()
- : new org.apache.tika.detect.CompositeEncodingDetector((List<EncodingDetector>) list))
+ .wrapWith(list -> {
+ if (list.isEmpty()) {
+ return new DefaultEncodingDetector();
+ } else if (list.size() == 1 && list.get(0) instanceof CompositeEncodingDetector) {
+ // Don't double-wrap if single item is already a CompositeEncodingDetector
+ // (e.g., DefaultEncodingDetector with exclusions)
+ return (EncodingDetector) list.get(0);
+ } else {
+ return new org.apache.tika.detect.CompositeEncodingDetector((List<EncodingDetector>) list);
+ }
+ })
.defaultProvider(DefaultEncodingDetector::new)
.register();
@@ -136,6 +147,7 @@ public class TikaLoader {
ComponentConfig.builder("translator", Translator.class)
.loadAsList()
.wrapWith(list -> list.isEmpty() ? null : (Translator) list.get(0))
+ .defaultProvider(DefaultTranslator::new)
.register();
}
@@ -158,8 +170,11 @@ public class TikaLoader {
// Pending configs for deferred creation of DefaultParser/DefaultDetector/DefaultEncodingDetector
// These are created in post-processing to avoid double-creation
private JsonNode pendingDefaultParserConfig;
+ private int pendingDefaultParserIndex = -1;
private JsonNode pendingDefaultDetectorConfig;
+ private int pendingDefaultDetectorIndex = -1;
private JsonNode pendingDefaultEncodingDetectorConfig;
+ private int pendingDefaultEncodingDetectorIndex = -1;
private TikaLoader(TikaJsonConfig config, ClassLoader classLoader) {
this.config = config;
@@ -599,7 +614,14 @@ public class TikaLoader {
// If empty and has default, use default
if (componentList.isEmpty()) {
if (componentConfig.hasDefault()) {
- return componentConfig.getDefault();
+ T defaultComponent = componentConfig.getDefault();
+ // For Parser defaults, configure dependencies (encoding detector, renderer)
+ if (componentClass == Parser.class && defaultComponent instanceof Parser) {
+ List<Parser> singletonList = new ArrayList<>();
+ singletonList.add((Parser) defaultComponent);
+ configureParserDependencies(singletonList);
+ }
+ return defaultComponent;
}
// For components that wrap empty lists
if (componentConfig.hasListWrapper()) {
@@ -693,21 +715,29 @@ public class TikaLoader {
}
List<T> components = new ArrayList<>();
+ int index = 0;
for (Map.Entry<String, JsonNode> entry : entries) {
String typeName = entry.getKey();
JsonNode configNode = entry.getValue();
// Defer DefaultParser/DefaultDetector/DefaultEncodingDetector creation to post-processing
+ // Track the index where it was defined to preserve ordering
if ("default-parser".equals(typeName) && componentClass == Parser.class) {
pendingDefaultParserConfig = configNode;
+ pendingDefaultParserIndex = index;
+ index++;
continue;
}
if ("default-detector".equals(typeName) && componentClass == Detector.class) {
pendingDefaultDetectorConfig = configNode;
+ pendingDefaultDetectorIndex = index;
+ index++;
continue;
}
if ("default-encoding-detector".equals(typeName) && componentClass == EncodingDetector.class) {
pendingDefaultEncodingDetectorConfig = configNode;
+ pendingDefaultEncodingDetectorIndex = index;
+ index++;
continue;
}
@@ -719,6 +749,7 @@ public class TikaLoader {
// Deserialize using Jackson (TikaModule handles type resolution)
T component = objectMapper.treeToValue(wrapperNode, componentClass);
components.add(component);
+ index++;
} catch (Exception e) {
throw new TikaConfigException(
"Failed to load " + componentClass.getSimpleName() + ": " + typeName, e);
@@ -733,6 +764,8 @@ public class TikaLoader {
/**
* Creates DefaultParser (if configured) with config exclusions + auto-exclusions.
* Auto-exclusions are the explicit parser types to prevent duplicates.
+ * Inserts at the original position to preserve ordering.
+ * Also applies mime filtering (_mime-include/_mime-exclude) if configured.
* <p>
* Note: EncodingDetector and Renderer are configured later in configureParserDependencies.
*/
@@ -752,19 +785,49 @@ public class TikaLoader {
}
// Create DefaultParser with all exclusions
- List<Parser> result = new ArrayList<>(parsers);
- result.add(new DefaultParser(
+ Parser defaultParser = new DefaultParser(
getMediaTypeRegistry(),
new ServiceLoader(classLoader),
- exclusions));
+ exclusions);
+
+ // Apply mime filtering if configured
+ Set<MediaType> includeTypes = extractMimeTypes(pendingDefaultParserConfig, "_mime-include");
+ Set<MediaType> excludeTypes = extractMimeTypes(pendingDefaultParserConfig, "_mime-exclude");
+ if (!includeTypes.isEmpty() || !excludeTypes.isEmpty()) {
+ defaultParser = ParserDecorator.withMimeFilters(defaultParser, includeTypes, excludeTypes);
+ }
+
+ // Insert at original position to preserve ordering
+ List<Parser> result = new ArrayList<>(parsers);
+ int insertIndex = Math.min(pendingDefaultParserIndex, result.size());
+ result.add(insertIndex, defaultParser);
pendingDefaultParserConfig = null;
+ pendingDefaultParserIndex = -1;
return result;
}
+ /**
+ * Extracts mime types from a config node field.
+ */
+ private Set<MediaType> extractMimeTypes(JsonNode configNode, String fieldName) {
+ Set<MediaType> types = new HashSet<>();
+ if (configNode == null || !configNode.has(fieldName)) {
+ return types;
+ }
+ JsonNode arrayNode = configNode.get(fieldName);
+ if (arrayNode.isArray()) {
+ for (JsonNode typeNode : arrayNode) {
+ types.add(MediaType.parse(typeNode.asText()));
+ }
+ }
+ return types;
+ }
+
/**
* Creates DefaultDetector (if configured) with config exclusions + auto-exclusions.
* Auto-exclusions are the explicit detector types to prevent duplicates.
+ * Inserts at the original position to preserve ordering.
*/
@SuppressWarnings("unchecked")
private List<Detector> applyDetectorAutoExclusions(List<Detector> detectors) throws IOException {
@@ -782,19 +845,25 @@ public class TikaLoader {
}
// Create DefaultDetector with all exclusions
- List<Detector> result = new ArrayList<>(detectors);
- result.add(new DefaultDetector(
+ DefaultDetector defaultDetector = new DefaultDetector(
getMimeTypes(),
new ServiceLoader(classLoader),
- exclusions));
+ exclusions);
+
+ // Insert at original position to preserve ordering
+ List<Detector> result = new ArrayList<>(detectors);
+ int insertIndex = Math.min(pendingDefaultDetectorIndex, result.size());
+ result.add(insertIndex, defaultDetector);
pendingDefaultDetectorConfig = null;
+ pendingDefaultDetectorIndex = -1;
return result;
}
/**
* Creates DefaultEncodingDetector (if configured) with config exclusions + auto-exclusions.
* Auto-exclusions are the explicit encoding detector types to prevent duplicates.
+ * Inserts at the original position to preserve ordering.
*/
@SuppressWarnings("unchecked")
private List<EncodingDetector> applyEncodingDetectorAutoExclusions(List<EncodingDetector> encodingDetectors)
@@ -814,12 +883,17 @@ public class TikaLoader {
}
// Create DefaultEncodingDetector with all exclusions
- List<EncodingDetector> result = new ArrayList<>(encodingDetectors);
- result.add(new DefaultEncodingDetector(
+ DefaultEncodingDetector defaultEncodingDetector = new DefaultEncodingDetector(
new ServiceLoader(classLoader),
- exclusions));
+ exclusions);
+
+ // Insert at original position to preserve ordering
+ List<EncodingDetector> result = new ArrayList<>(encodingDetectors);
+ int insertIndex = Math.min(pendingDefaultEncodingDetectorIndex, result.size());
+ result.add(insertIndex, defaultEncodingDetector);
pendingDefaultEncodingDetectorConfig = null;
+ pendingDefaultEncodingDetectorIndex = -1;
return result;
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
index 14f0fda5f..b50709702 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
@@ -56,10 +56,12 @@ import org.apache.tika.language.translate.Translator;
import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.metadata.writefilter.MetadataWriteFilterFactory;
import org.apache.tika.mime.MediaType;
+import org.apache.tika.mime.MimeTypes;
import org.apache.tika.parser.DefaultParser;
import org.apache.tika.parser.Parser;
import org.apache.tika.parser.ParserDecorator;
import org.apache.tika.renderer.Renderer;
+import org.apache.tika.sax.ContentHandlerDecoratorFactory;
import org.apache.tika.serialization.serdes.DefaultDetectorSerializer;
import org.apache.tika.serialization.serdes.DefaultParserSerializer;
@@ -100,6 +102,7 @@ public class TikaModule extends SimpleModule {
COMPACT_FORMAT_INTERFACES.add(DigesterFactory.class);
COMPACT_FORMAT_INTERFACES.add(EmbeddedDocumentExtractorFactory.class);
COMPACT_FORMAT_INTERFACES.add(MetadataWriteFilterFactory.class);
+ COMPACT_FORMAT_INTERFACES.add(ContentHandlerDecoratorFactory.class);
}
/**
@@ -282,6 +285,9 @@ public class TikaModule extends SimpleModule {
} else if (clazz == DefaultDetector.class) {
throw new IOException("DefaultDetector must be loaded via TikaLoader, not directly " +
"via Jackson deserialization. Use TikaLoader.load() to load configuration.");
+ } else if (clazz == MimeTypes.class) {
+ // MimeTypes must use the singleton to have all type definitions loaded
+ instance = MimeTypes.getDefaultMimeTypes();
} else if (cleanedConfig == null || cleanedConfig.isEmpty()) {
// If no config, use default constructor
instance = clazz.getDeclaredConstructor().newInstance();
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
index 3839ce57a..997fe6e4f 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
@@ -121,20 +121,37 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
String componentName = fieldNames.next();
JsonNode configNode = typedNode.get(componentName);
+ Class<?> configClass = null;
+
+ // First, try component registry lookup (for friendly names like "pdf-parser-config")
try {
- // Look up the class for this component name
- Class<?> configClass = ComponentNameResolver.resolveClass(
+ configClass = ComponentNameResolver.resolveClass(
componentName, ParseContextDeserializer.class.getClassLoader());
+ } catch (ClassNotFoundException e) {
+ // Not in registry, try as FQCN
+ }
+
+ // If not found in registry, try as fully qualified class name
+ if (configClass == null) {
+ try {
+ configClass = Class.forName(componentName);
+ } catch (ClassNotFoundException e) {
+ LOG.warn("Could not find class for typed component '{}', storing as JSON config",
+ componentName);
+ // Fall back to storing as JSON config
+ parseContext.setJsonConfig(componentName, mapper.writeValueAsString(configNode));
+ continue;
+ }
+ }
- // Deserialize and add to context
+ // Deserialize and add to context
+ try {
Object config = mapper.treeToValue(configNode, configClass);
parseContext.set((Class) configClass, config);
-
LOG.debug("Deserialized typed object '{}' -> {}", componentName, configClass.getName());
- } catch (ClassNotFoundException e) {
- LOG.warn("Could not find class for typed component '{}', storing as JSON config",
- componentName);
- // Fall back to storing as JSON config
+ } catch (Exception e) {
+ LOG.warn("Failed to deserialize typed component '{}' as {}, storing as JSON config",
+ componentName, configClass.getName(), e);
parseContext.setJsonConfig(componentName, mapper.writeValueAsString(configNode));
}
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
index 0e966f046..903c48f3e 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
@@ -62,16 +62,26 @@ public class ParseContextSerializer extends JsonSerializer<ParseContext> {
for (Map.Entry<String, Object> entry : contextMap.entrySet()) {
String className = entry.getKey();
- String componentName = findComponentName(className);
- if (componentName != null) {
- if (!hasTypedObjects) {
- gen.writeFieldName(TYPED);
- gen.writeStartObject();
- hasTypedObjects = true;
- }
- gen.writeFieldName(componentName);
- gen.writeRawValue(mapper.writeValueAsString(entry.getValue()));
+ Object value = entry.getValue();
+
+ // Skip null values
+ if (value == null) {
+ continue;
+ }
+
+ // Try to find a friendly component name, otherwise use FQCN
+ String keyName = findComponentName(className);
+ if (keyName == null) {
+ keyName = className;
+ }
+
+ if (!hasTypedObjects) {
+ gen.writeFieldName(TYPED);
+ gen.writeStartObject();
+ hasTypedObjects = true;
}
+ gen.writeFieldName(keyName);
+ gen.writeRawValue(mapper.writeValueAsString(value));
}
if (hasTypedObjects) {
(tika) 05/05: TIKA-4585 -- further progress
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4585-simplify-serialization
in repository https://gitbox.apache.org/repos/asf/tika.git
commit 05e3fc4d9056330f51002aee96519970127bea49
Author: tallison <ta...@apache.org>
AuthorDate: Sat Dec 20 23:29:18 2025 -0500
TIKA-4585 -- further progress
---
.../test/java/org/apache/tika/cli/TikaCLITest.java | 13 +++---
.../java/org/apache/tika/parser/ParseContext.java | 13 +++++-
.../filter/AttachmentCountingListFilter.java | 47 ++++++++++++++++++++++
.../tika/metadata/filter/MockUpperCaseFilter.java | 40 ++++++++++++++++++
.../tika/server/core/resource/TikaResource.java | 14 ++++++-
.../core/TikaServerPipesIntegrationTest.java | 2 +-
.../standard/RecursiveMetadataResourceTest.java | 5 ++-
.../tika/server/standard/TikaParsersTest.java | 10 +++--
.../apache/tika/server/standard/TikaPipesTest.java | 4 +-
9 files changed, 128 insertions(+), 20 deletions(-)
diff --git a/tika-app/src/test/java/org/apache/tika/cli/TikaCLITest.java b/tika-app/src/test/java/org/apache/tika/cli/TikaCLITest.java
index 73e167d60..f09338059 100644
--- a/tika-app/src/test/java/org/apache/tika/cli/TikaCLITest.java
+++ b/tika-app/src/test/java/org/apache/tika/cli/TikaCLITest.java
@@ -38,8 +38,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@@ -237,7 +235,7 @@ public class TikaCLITest {
public void testJsonMetadataPrettyPrintOutput() throws Exception {
String json = getParamOutContent("--json", "-r", resourcePrefix + "testJsonMultipleInts.html");
- assertTrue(json.contains("org.apache.tika.parser.CompositeParser\", \"org.apache.tika.parser.html.JSoupParser"));
+ assertTrue(json.contains("org.apache.tika.parser.DefaultParser\", \"org.apache.tika.parser.html.JSoupParser"));
//test pretty-print alphabetic sort of keys
int enc = json.indexOf("\"Content-Encoding\"");
int fb = json.indexOf("fb:admins");
@@ -413,22 +411,23 @@ public class TikaCLITest {
final Set<String> names = new HashSet<>();
Files.walkFileTree(extractDir, new FileVisitor<Path>() {
@Override
- public @NotNull FileVisitResult preVisitDirectory(Path path, @NotNull BasicFileAttributes basicFileAttributes) throws IOException {
+ public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes) throws IOException {
return FileVisitResult.CONTINUE;
}
- public @NotNull FileVisitResult visitFile(Path path, @NotNull BasicFileAttributes basicFileAttributes) throws IOException {
+ @Override
+ public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException {
names.add(extractDir.relativize(path).toString().replace('\\', '/'));
return FileVisitResult.CONTINUE;
}
@Override
- public @NotNull FileVisitResult visitFileFailed(Path path, @NotNull IOException e) throws IOException {
+ public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
- public @NotNull FileVisitResult postVisitDirectory(Path path, @Nullable IOException e) throws IOException {
+ public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException {
return FileVisitResult.CONTINUE;
}
});
diff --git a/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java b/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
index 137fa9bca..c4587c1fd 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
@@ -51,8 +51,9 @@ public class ParseContext implements Serializable {
/**
* Cache of resolved objects from jsonConfigs, keyed by component name.
* This is ignored during serialization to preserve round-trip fidelity.
+ * Note: Not final because Java serialization bypasses constructor initialization.
*/
- private final transient Map<String, Object> resolvedConfigs = new HashMap<>();
+ private transient Map<String, Object> resolvedConfigs = new HashMap<>();
/**
* Adds the given value to the context as an implementation of the given
@@ -120,7 +121,9 @@ public class ParseContext implements Serializable {
jsonConfigs.put(name, config);
} else {
jsonConfigs.remove(name);
- resolvedConfigs.remove(name);
+ if (resolvedConfigs != null) {
+ resolvedConfigs.remove(name);
+ }
}
}
@@ -178,6 +181,9 @@ public class ParseContext implements Serializable {
*/
@SuppressWarnings("unchecked")
public <T> T getResolvedConfig(String name) {
+ if (resolvedConfigs == null) {
+ return null;
+ }
return (T) resolvedConfigs.get(name);
}
@@ -191,6 +197,9 @@ public class ParseContext implements Serializable {
* @since Apache Tika 4.0
*/
public void setResolvedConfig(String name, Object config) {
+ if (resolvedConfigs == null) {
+ resolvedConfigs = new HashMap<>();
+ }
if (config != null) {
resolvedConfigs.put(name, config);
} else {
diff --git a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/metadata/filter/AttachmentCountingListFilter.java b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/metadata/filter/AttachmentCountingListFilter.java
new file mode 100644
index 000000000..fdf4f1c22
--- /dev/null
+++ b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/metadata/filter/AttachmentCountingListFilter.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.metadata.filter;
+
+import java.util.List;
+
+import org.apache.tika.config.TikaComponent;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.metadata.Metadata;
+
+@TikaComponent
+public class AttachmentCountingListFilter extends MetadataFilter {
+
+ private Integer count = 0;
+
+ @Override
+ public List<Metadata> filter(List<Metadata> metadataList) throws TikaException {
+ if (metadataList == null || metadataList.isEmpty()) {
+ return metadataList;
+ }
+ metadataList.get(0).set("X-TIKA:attachment_count", Integer.toString(metadataList.size() - 1));
+ count += metadataList.size();
+ return metadataList;
+ }
+
+ public Integer getCount() {
+ return count;
+ }
+
+ public void setCount(Integer count) {
+ this.count = count;
+ }
+}
diff --git a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/metadata/filter/MockUpperCaseFilter.java b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/metadata/filter/MockUpperCaseFilter.java
new file mode 100644
index 000000000..a57b5dd7d
--- /dev/null
+++ b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/metadata/filter/MockUpperCaseFilter.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.metadata.filter;
+
+import java.util.Locale;
+
+import org.apache.tika.config.TikaComponent;
+import org.apache.tika.metadata.Metadata;
+
+/**
+ * Mock Filter for testing uppercasing of all values
+ */
+@TikaComponent
+public class MockUpperCaseFilter extends MetadataFilterBase {
+
+ @Override
+ protected void filter(Metadata metadata) {
+ for (String n : metadata.names()) {
+ String[] vals = metadata.getValues(n);
+ metadata.remove(n);
+ for (String val : vals) {
+ metadata.add(n, val.toUpperCase(Locale.US));
+ }
+ }
+ }
+}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
index 71327764a..4729e66a4 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
@@ -62,6 +62,7 @@ import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.Tika;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.TikaTaskTimeout;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.exception.EncryptedDocumentException;
@@ -78,8 +79,8 @@ import org.apache.tika.sax.BodyContentHandler;
import org.apache.tika.sax.ExpandedTitleContentHandler;
import org.apache.tika.sax.RichTextContentHandler;
import org.apache.tika.sax.boilerpipe.BoilerpipeContentHandler;
-import org.apache.tika.serialization.serdes.ParseContextDeserializer;
import org.apache.tika.serialization.ParseContextUtils;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
import org.apache.tika.server.core.InputStreamFactory;
import org.apache.tika.server.core.ServerStatus;
import org.apache.tika.server.core.TikaServerConfig;
@@ -148,8 +149,17 @@ public class TikaResource {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(configJson);
// Use root directly - the JSON should contain parser configs at the top level
- ParseContext configuredContext = ParseContextDeserializer.readParseContext(root);
+ ParseContext configuredContext = ParseContextDeserializer.readParseContext(root, mapper);
+
+ // Copy jsonConfigs first (for SelfConfiguring parsers like PDFParser)
+ for (Map.Entry<String, JsonConfig> entry : configuredContext.getJsonConfigs().entrySet()) {
+ context.setJsonConfig(entry.getKey(), entry.getValue());
+ }
+
+ // Then resolve all configs to typed objects
ParseContextUtils.resolveAll(configuredContext, Thread.currentThread().getContextClassLoader());
+
+ // Copy resolved typed objects from contextMap
for (Map.Entry<String, Object> entry : configuredContext.getContextMap().entrySet()) {
try {
Class<?> clazz = Class.forName(entry.getKey());
diff --git a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerPipesIntegrationTest.java b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerPipesIntegrationTest.java
index 5ba494c44..e7957c9df 100644
--- a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerPipesIntegrationTest.java
+++ b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerPipesIntegrationTest.java
@@ -222,7 +222,7 @@ public class TikaServerPipesIntegrationTest extends IntegrationTestBase {
private String getJsonStringWithTimeout(String fileName, long timeoutMillis) throws IOException {
ParseContext parseContext = new ParseContext();
parseContext.set(HandlerConfig.class, DEFAULT_HANDLER_CONFIG);
- parseContext.addConfig("tika-task-timeout", "{\"timeoutMillis\":" + timeoutMillis + "}");
+ parseContext.setJsonConfig("tika-task-timeout", "{\"timeoutMillis\":" + timeoutMillis + "}");
FetchEmitTuple t = new FetchEmitTuple(fileName,
new FetchKey(CXFTestBase.FETCHER_ID, fileName),
diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java
index 33aae76b8..8dcd90a29 100644
--- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java
+++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java
@@ -118,9 +118,10 @@ public class RecursiveMetadataResourceTest extends CXFTestBase {
.get(0)
.getValues(TikaCoreProperties.TIKA_PARSED_BY);
//make sure the CompressorParser doesn't show up here
- assertEquals(2, parsedBy.length);
+ assertEquals(3, parsedBy.length);
assertEquals("org.apache.tika.parser.CompositeParser", parsedBy[0]);
- assertEquals("org.apache.tika.parser.microsoft.ooxml.OOXMLParser", parsedBy[1]);
+ assertEquals("org.apache.tika.parser.DefaultParser", parsedBy[1]);
+ assertEquals("org.apache.tika.parser.microsoft.ooxml.OOXMLParser", parsedBy[2]);
//test that the rest is as it should be
assertEquals(12, metadataList.size());
diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaParsersTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaParsersTest.java
index 64a5e090e..1eae7ba45 100644
--- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaParsersTest.java
+++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaParsersTest.java
@@ -100,9 +100,9 @@ public class TikaParsersTest extends CXFTestBase {
String text = getStringFromInputStream((InputStream) response.getEntity());
assertContains("Composite", text);
- assertContains("<h3>OpusParser", text);
- assertContains("<h3>PackageParser", text);
- assertContains("<h3>OOXMLParser", text);
+ assertContains("<h4>OpusParser", text);
+ assertContains("<h4>PackageParser", text);
+ assertContains("<h4>OOXMLParser", text);
assertContains(OpusParser.class.getName(), text);
assertContains(PackageParser.class.getName(), text);
@@ -145,7 +145,9 @@ public class TikaParsersTest extends CXFTestBase {
assertEquals(Boolean.TRUE, json.get("composite"));
// At least 20 child parsers which aren't composite, except for CompositeExternalParser
- List<Object> children = (List) json.get("children");
+ List<Object> wrapper = (List) json.get("children");
+ Map<String, Object> firstItem = (Map) wrapper.get(0);
+ List<Object> children = (List) firstItem.get("children");
assertTrue(children.size() >= 2);
boolean hasOpus = false, hasOOXML = false, hasZip = false;
int nonComposite = 0;
diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaPipesTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaPipesTest.java
index ac82d820a..3761fb0bf 100644
--- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaPipesTest.java
+++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaPipesTest.java
@@ -182,7 +182,7 @@ public class TikaPipesTest extends CXFTestBase {
public void testConcatenated() throws Exception {
ParseContext parseContext = new ParseContext();
// Use addConfig with JSON for handler-config
- parseContext.addConfig("handler-config",
+ parseContext.setJsonConfig("handler-config",
"{\"type\": \"TEXT\", \"parseMode\": \"CONCATENATE\", \"writeLimit\": -1, \"maxEmbeddedResources\": -1, \"throwOnWriteLimitReached\": true}");
FetchEmitTuple t = new FetchEmitTuple("myId", new FetchKey(FETCHER_ID, "test_recursive_embedded.docx"),
@@ -212,7 +212,7 @@ public class TikaPipesTest extends CXFTestBase {
public void testPDFConfig() throws Exception {
ParseContext parseContext = new ParseContext();
// Configure PDFParser via JSON config (pdf-parser is self-configuring)
- parseContext.addConfig("pdf-parser", "{\"sortByPosition\": true}");
+ parseContext.setJsonConfig("pdf-parser", "{\"sortByPosition\": true}");
FetchEmitTuple t = new FetchEmitTuple("myId", new FetchKey(FETCHER_ID, TEST_TWO_BOXES_PDF),
new EmitKey(EMITTER_JSON_ID, ""), new Metadata(), parseContext);
(tika) 02/05: TIKA-4585 -- further progress - WIP
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4585-simplify-serialization
in repository https://gitbox.apache.org/repos/asf/tika.git
commit 47df95adc04778a99e0ed7272889e8dac268f70f
Author: tallison <ta...@apache.org>
AuthorDate: Sat Dec 20 15:48:41 2025 -0500
TIKA-4585 -- further progress - WIP
---
.../java/org/apache/tika/parser/ParseContext.java | 10 ++-
tika-pipes/tika-pipes-core/pom.xml | 5 ++
.../serialization/FetchEmitTupleDeserializer.java | 4 +-
.../tika/serialization/ParseContextUtils.java | 2 +-
.../serdes/ParseContextDeserializer.java | 75 ++++++++++++++++++----
.../serdes/ParseContextSerializer.java | 57 ++++++++++++++--
6 files changed, 131 insertions(+), 22 deletions(-)
diff --git a/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java b/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
index db4a0e157..137fa9bca 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
@@ -134,7 +134,15 @@ public class ParseContext implements Serializable {
* @since Apache Tika 4.0
*/
public void setJsonConfig(String name, String json) {
- setJsonConfig(name, json != null ? () -> json : null);
+ setJsonConfig(name, json != null ? new StringJsonConfig(json) : null);
+ }
+
+ /**
+ * A simple Serializable implementation of JsonConfig that holds a JSON string.
+ * This is used internally to ensure JSON configs can be serialized via Java serialization.
+ */
+ private record StringJsonConfig(String json) implements JsonConfig, Serializable {
+ private static final long serialVersionUID = 1L;
}
/**
diff --git a/tika-pipes/tika-pipes-core/pom.xml b/tika-pipes/tika-pipes-core/pom.xml
index b5c873221..d3b5fe2f9 100644
--- a/tika-pipes/tika-pipes-core/pom.xml
+++ b/tika-pipes/tika-pipes-core/pom.xml
@@ -32,6 +32,11 @@
<url>https://tika.apache.org/</url>
<dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-annotation-processor</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>tika-pipes-api</artifactId>
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java
index be76398d7..a1d531f5f 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java
@@ -36,6 +36,7 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
@@ -50,6 +51,7 @@ public class FetchEmitTupleDeserializer extends JsonDeserializer<FetchEmitTuple>
@Override
public FetchEmitTuple deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
JsonNode root = jsonParser.readValueAsTree();
+ ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
String id = readVal(ID, root, null, true);
String fetcherId = readVal(FETCHER, root, null, true);
@@ -60,7 +62,7 @@ public class FetchEmitTupleDeserializer extends JsonDeserializer<FetchEmitTuple>
long fetchRangeEnd = readLong(FETCH_RANGE_END, root, -1l, false);
Metadata metadata = readMetadata(root);
JsonNode parseContextNode = root.get(PARSE_CONTEXT);
- ParseContext parseContext = parseContextNode == null ? new ParseContext() : ParseContextDeserializer.readParseContext(parseContextNode);
+ ParseContext parseContext = parseContextNode == null ? new ParseContext() : ParseContextDeserializer.readParseContext(parseContextNode, mapper);
// Resolve all friendly-named components from jsonConfigs to actual objects
ParseContextUtils.resolveAll(parseContext, FetchEmitTupleDeserializer.class.getClassLoader());
FetchEmitTuple.ON_PARSE_EXCEPTION onParseException = readOnParseException(root);
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
index d31d5cd50..e530c1951 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
@@ -152,7 +152,7 @@ public class ParseContextUtils {
// Determine the context key
Class<?> contextKey = determineContextKey(info, friendlyName);
- // Deserialize and cache
+ // Deserialize and cache in resolvedConfigs, also add to context
Object instance = MAPPER.readValue(jsonConfig.json(), info.componentClass());
context.setResolvedConfig(friendlyName, instance);
context.set((Class) contextKey, instance);
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
index 2e566f4fa..3839ce57a 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
@@ -17,6 +17,7 @@
package org.apache.tika.serialization.serdes;
import static org.apache.tika.serialization.serdes.ParseContextSerializer.PARSE_CONTEXT;
+import static org.apache.tika.serialization.serdes.ParseContextSerializer.TYPED;
import java.io.IOException;
import java.util.Iterator;
@@ -26,26 +27,35 @@ import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.apache.tika.parser.ParseContext;
-import org.apache.tika.serialization.ConfigDeserializer;
+import org.apache.tika.serialization.ComponentNameResolver;
/**
* Deserializes ParseContext from JSON.
* <p>
- * Each field in the JSON object is stored as a JSON config in the ParseContext.
- * Resolution to typed objects happens later via {@link ConfigDeserializer}.
+ * Handles two types of entries:
+ * <ul>
+ * <li>"typed" section: Deserialized directly to typed objects in the context map</li>
+ * <li>Other entries: Stored as JSON configs for lazy resolution</li>
+ * </ul>
* <p>
* Example input:
* <pre>
* {
- * "pdf-parser": {"ocrStrategy": "AUTO"},
- * "handler-config": {"type": "XML", "parseMode": "RMETA"}
+ * "typed": {
+ * "handler-config": {"type": "XML", "parseMode": "RMETA"}
+ * },
+ * "metadata-filters": ["mock-upper-case-filter"]
* }
* </pre>
*/
public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
+ private static final Logger LOG = LoggerFactory.getLogger(ParseContextDeserializer.class);
+
@Override
public ParseContext deserialize(JsonParser jsonParser, DeserializationContext ctxt)
throws IOException {
@@ -56,12 +66,12 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
/**
* Deserializes a ParseContext from a JsonNode.
* <p>
- * Each field is stored as a JSON config string in the ParseContext's jsonConfigs map.
- * The configs can later be resolved to typed objects via {@link ConfigDeserializer}.
+ * The "typed" section is deserialized directly to typed objects in the context map.
+ * All other fields are stored as JSON config strings for lazy resolution.
*
* @param jsonNode the JSON node containing the ParseContext data
- * @param mapper the ObjectMapper for serializing field values back to JSON strings
- * @return the deserialized ParseContext with jsonConfigs populated
+ * @param mapper the ObjectMapper for deserializing typed objects
+ * @return the deserialized ParseContext
* @throws IOException if deserialization fails
*/
public static ParseContext readParseContext(JsonNode jsonNode, ObjectMapper mapper)
@@ -78,16 +88,55 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
return parseContext;
}
- // Store each field as a JSON config
Iterator<String> fieldNames = contextNode.fieldNames();
while (fieldNames.hasNext()) {
String name = fieldNames.next();
JsonNode value = contextNode.get(name);
- // Store the JSON string for later resolution
- String json = mapper.writeValueAsString(value);
- parseContext.setJsonConfig(name, json);
+
+ if (TYPED.equals(name)) {
+ // Deserialize typed objects directly to context map
+ deserializeTypedObjects(value, parseContext, mapper);
+ } else {
+ // Store as JSON config for lazy resolution
+ String json = mapper.writeValueAsString(value);
+ parseContext.setJsonConfig(name, json);
+ }
}
return parseContext;
}
+
+ /**
+ * Deserializes the "typed" section into typed objects in the context map.
+ */
+ @SuppressWarnings("unchecked")
+ private static void deserializeTypedObjects(JsonNode typedNode, ParseContext parseContext,
+ ObjectMapper mapper) throws IOException {
+ if (!typedNode.isObject()) {
+ return;
+ }
+
+ Iterator<String> fieldNames = typedNode.fieldNames();
+ while (fieldNames.hasNext()) {
+ String componentName = fieldNames.next();
+ JsonNode configNode = typedNode.get(componentName);
+
+ try {
+ // Look up the class for this component name
+ Class<?> configClass = ComponentNameResolver.resolveClass(
+ componentName, ParseContextDeserializer.class.getClassLoader());
+
+ // Deserialize and add to context
+ Object config = mapper.treeToValue(configNode, configClass);
+ parseContext.set((Class) configClass, config);
+
+ LOG.debug("Deserialized typed object '{}' -> {}", componentName, configClass.getName());
+ } catch (ClassNotFoundException e) {
+ LOG.warn("Could not find class for typed component '{}', storing as JSON config",
+ componentName);
+ // Fall back to storing as JSON config
+ parseContext.setJsonConfig(componentName, mapper.writeValueAsString(configNode));
+ }
+ }
+ }
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
index f0648e5f8..0e966f046 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
@@ -21,42 +21,87 @@ import java.util.Map;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.apache.tika.config.JsonConfig;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.serialization.ComponentNameResolver;
/**
* Serializes ParseContext to JSON.
* <p>
- * Serializes the jsonConfigs map directly - each entry becomes a field in the output.
- * The resolvedConfigs cache is intentionally ignored to preserve round-trip fidelity.
+ * Typed objects from the context map are serialized under a "typed" key.
+ * JSON configs are serialized at the top level.
* <p>
* Example output:
* <pre>
* {
- * "pdf-parser": {"ocrStrategy": "AUTO"},
- * "handler-config": {"type": "XML", "parseMode": "RMETA"}
+ * "typed": {
+ * "handler-config": {"type": "XML", "parseMode": "RMETA"}
+ * },
+ * "metadata-filters": ["mock-upper-case-filter"]
* }
* </pre>
*/
public class ParseContextSerializer extends JsonSerializer<ParseContext> {
public static final String PARSE_CONTEXT = "parseContext";
+ public static final String TYPED = "typed";
@Override
public void serialize(ParseContext parseContext, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeStartObject();
- // Serialize all JSON configs - this is the source of truth for round-trip
+ ObjectMapper mapper = (ObjectMapper) gen.getCodec();
+
+ // First, serialize typed objects from the context map under "typed" key
+ Map<String, Object> contextMap = parseContext.getContextMap();
+ boolean hasTypedObjects = false;
+
+ for (Map.Entry<String, Object> entry : contextMap.entrySet()) {
+ String className = entry.getKey();
+ String componentName = findComponentName(className);
+ if (componentName != null) {
+ if (!hasTypedObjects) {
+ gen.writeFieldName(TYPED);
+ gen.writeStartObject();
+ hasTypedObjects = true;
+ }
+ gen.writeFieldName(componentName);
+ gen.writeRawValue(mapper.writeValueAsString(entry.getValue()));
+ }
+ }
+
+ if (hasTypedObjects) {
+ gen.writeEndObject();
+ }
+
+ // Then, serialize JSON configs at the top level
Map<String, JsonConfig> jsonConfigs = parseContext.getJsonConfigs();
for (Map.Entry<String, JsonConfig> entry : jsonConfigs.entrySet()) {
gen.writeFieldName(entry.getKey());
- // Write the JSON config as raw value (it's already valid JSON)
gen.writeRawValue(entry.getValue().json());
}
gen.writeEndObject();
}
+
+ /**
+ * Finds the component name for a class.
+ * Uses ComponentNameResolver for registry lookup. Only classes registered
+ * in a component registry will be serialized.
+ *
+ * @param className the fully qualified class name
+ * @return the component name, or null if not registered
+ */
+ private String findComponentName(String className) {
+ try {
+ Class<?> clazz = Class.forName(className);
+ return ComponentNameResolver.getFriendlyName(clazz);
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
}
(tika) 03/05: TIKA-4585 -- further progress - WIP
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4585-simplify-serialization
in repository https://gitbox.apache.org/repos/asf/tika.git
commit fb4cb1c5a67bfcacfba73ce1b7c995f9cc61b387
Author: tallison <ta...@apache.org>
AuthorDate: Sat Dec 20 21:28:22 2025 -0500
TIKA-4585 -- further progress - WIP
---
.../org/apache/tika/parser/image/PSDParser.java | 19 ++
.../apache/tika/parser/csv/TextAndCSVParser.java | 2 +-
.../apache/tika/parser/strings/StringsParser.java | 11 +-
.../org/apache/tika/config/loader/TikaLoader.java | 249 ++++++++++++++-------
.../org/apache/tika/serialization/TikaModule.java | 15 +-
.../serdes/DefaultDetectorDeserializer.java | 48 ----
.../serdes/DefaultParserDeserializer.java | 48 ----
.../serdes/SpiCompositeDeserializer.java | 97 --------
8 files changed, 210 insertions(+), 279 deletions(-)
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/PSDParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/PSDParser.java
index e77947e87..dc4e6072e 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/PSDParser.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/PSDParser.java
@@ -30,6 +30,8 @@ import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.TikaComponent;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.TikaMemoryLimitException;
@@ -70,6 +72,16 @@ public class PSDParser implements Parser {
private int maxDataLengthBytes = MAX_DATA_LENGTH_BYTES;
+ public PSDParser() {
+ }
+
+ public PSDParser(JsonConfig jsonConfig) {
+ PSDParserConfig config = ConfigDeserializer.buildConfig(jsonConfig, PSDParserConfig.class);
+ if (config != null && config.maxDataLengthBytes > 0) {
+ this.maxDataLengthBytes = config.maxDataLengthBytes;
+ }
+ }
+
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
}
@@ -271,4 +283,11 @@ public class PSDParser implements Parser {
return new String(data, 0, data.length - 1, US_ASCII);
}
}
+
+ /**
+ * Configuration class for PSDParser.
+ */
+ public static class PSDParserConfig {
+ public int maxDataLengthBytes;
+ }
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/csv/TextAndCSVParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/csv/TextAndCSVParser.java
index c5d0ffe48..66396adf2 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/csv/TextAndCSVParser.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/csv/TextAndCSVParser.java
@@ -70,7 +70,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
* {@link org.apache.commons.csv.CSVParser} is lost.
* </p>
*/
-@TikaComponent
+@TikaComponent(name = "text-and-csv-parser")
public class TextAndCSVParser extends AbstractEncodingDetectorParser {
static final MediaType CSV = MediaType.text("csv");
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/strings/StringsParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/strings/StringsParser.java
index 427eabbeb..aac74f624 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/strings/StringsParser.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-text-module/src/main/java/org/apache/tika/parser/strings/StringsParser.java
@@ -34,7 +34,9 @@ import org.apache.commons.io.IOUtils;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
+import org.apache.tika.config.ConfigDeserializer;
import org.apache.tika.config.Initializable;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.TikaComponent;
import org.apache.tika.detect.FileCommandDetector;
import org.apache.tika.exception.TikaConfigException;
@@ -67,7 +69,7 @@ public class StringsParser implements Parser, Initializable {
private static final Set<MediaType> SUPPORTED_TYPES =
Collections.singleton(MediaType.OCTET_STREAM);
- private final StringsConfig defaultStringsConfig = new StringsConfig();
+ private StringsConfig defaultStringsConfig = new StringsConfig();
private String filePath = "";
@@ -78,6 +80,13 @@ public class StringsParser implements Parser, Initializable {
private String stringsPath = "";
+ public StringsParser() {
+ }
+
+ public StringsParser(JsonConfig jsonConfig) {
+ defaultStringsConfig = ConfigDeserializer.buildConfig(jsonConfig, StringsConfig.class);
+ }
+
public static String getStringsProg() {
return SystemUtils.IS_OS_WINDOWS ? "strings.exe" : "strings";
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
index 5748d894b..42f9b5992 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
@@ -46,11 +46,14 @@ import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.metadata.filter.NoOpFilter;
import org.apache.tika.mime.MediaTypeRegistry;
import org.apache.tika.mime.MimeTypes;
+import org.apache.tika.parser.AbstractEncodingDetectorParser;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.AutoDetectParserConfig;
import org.apache.tika.parser.CompositeParser;
import org.apache.tika.parser.DefaultParser;
import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.ParserDecorator;
+import org.apache.tika.parser.RenderingParser;
import org.apache.tika.renderer.CompositeRenderer;
import org.apache.tika.renderer.Renderer;
import org.apache.tika.serialization.ComponentConfig;
@@ -152,6 +155,12 @@ public class TikaLoader {
private ConfigLoader configLoader;
private GlobalSettings globalSettings;
+ // Pending configs for deferred creation of DefaultParser/DefaultDetector/DefaultEncodingDetector
+ // These are created in post-processing to avoid double-creation
+ private JsonNode pendingDefaultParserConfig;
+ private JsonNode pendingDefaultDetectorConfig;
+ private JsonNode pendingDefaultEncodingDetectorConfig;
+
private TikaLoader(TikaJsonConfig config, ClassLoader classLoader) {
this.config = config;
this.classLoader = classLoader;
@@ -581,16 +590,10 @@ public class TikaLoader {
private <T> T loadComponent(ComponentConfig<T> componentConfig) throws TikaConfigException, IOException {
Class<T> componentClass = componentConfig.getComponentClass();
- // Handle dependencies: Parsers need EncodingDetectors and Renderers loaded first
- if (componentClass == Parser.class) {
- get(EncodingDetector.class);
- get(Renderer.class);
- }
-
// Load the component list
List<T> componentList = loadComponentList(componentConfig.getJsonField(), componentClass);
- // Apply post-processing (auto-exclusions for Parser/Detector)
+ // Apply post-processing (auto-exclusions for Parser/Detector, configure dependencies)
componentList = applyPostProcessing(componentClass, componentList);
// If empty and has default, use default
@@ -616,22 +619,66 @@ public class TikaLoader {
/**
* Applies post-processing to component lists.
- * Currently handles auto-exclusions for Parser and Detector.
+ * Handles auto-exclusions and deferred creation for Parser, Detector, and EncodingDetector.
+ * Also sets EncodingDetector and Renderer on parsers that implement the appropriate interfaces.
*/
@SuppressWarnings("unchecked")
- private <T> List<T> applyPostProcessing(Class<T> componentClass, List<T> list) {
+ private <T> List<T> applyPostProcessing(Class<T> componentClass, List<T> list) throws IOException {
if (componentClass == Parser.class) {
- return (List<T>) applyParserAutoExclusions((List<Parser>) list);
+ List<Parser> parsers = applyParserAutoExclusions((List<Parser>) list);
+ configureParserDependencies(parsers);
+ return (List<T>) parsers;
} else if (componentClass == Detector.class) {
return (List<T>) applyDetectorAutoExclusions((List<Detector>) list);
+ } else if (componentClass == EncodingDetector.class) {
+ return (List<T>) applyEncodingDetectorAutoExclusions((List<EncodingDetector>) list);
}
return list;
}
+ /**
+ * Configures EncodingDetector and Renderer on all parsers.
+ * Recursively walks through CompositeParser children (including DefaultParser).
+ */
+ private void configureParserDependencies(List<Parser> parsers) throws IOException {
+ EncodingDetector encodingDetector = get(EncodingDetector.class);
+ Renderer renderer = get(Renderer.class);
+
+ for (Parser parser : parsers) {
+ // Recursively configure all parsers including DefaultParser's children
+ configureParserRecursively(parser, encodingDetector, renderer);
+ }
+ }
+
+ /**
+ * Recursively configures a parser and its children with EncodingDetector and Renderer.
+ */
+ private void configureParserRecursively(Parser parser, EncodingDetector encodingDetector,
+ Renderer renderer) {
+ if (encodingDetector != null && parser instanceof AbstractEncodingDetectorParser) {
+ ((AbstractEncodingDetectorParser) parser).setEncodingDetector(encodingDetector);
+ }
+ if (renderer != null && parser instanceof RenderingParser) {
+ ((RenderingParser) parser).setRenderer(renderer);
+ }
+ if (parser instanceof CompositeParser) {
+ for (Parser child : ((CompositeParser) parser).getAllComponentParsers()) {
+ configureParserRecursively(child, encodingDetector, renderer);
+ }
+ } else if (parser instanceof ParserDecorator) {
+ configureParserRecursively(((ParserDecorator) parser).getWrappedParser(),
+ encodingDetector, renderer);
+ }
+ }
+
// ==================== Component List Loading ====================
/**
* Loads a list of components from the JSON configuration.
+ * <p>
+ * DefaultParser and DefaultDetector are handled specially - their configs are stored
+ * for deferred creation in post-processing to avoid double-creation when auto-exclusions
+ * are needed.
*
* @param jsonField the JSON field name (e.g., "parsers", "detectors")
* @param componentClass the component class
@@ -650,6 +697,20 @@ public class TikaLoader {
String typeName = entry.getKey();
JsonNode configNode = entry.getValue();
+ // Defer DefaultParser/DefaultDetector/DefaultEncodingDetector creation to post-processing
+ if ("default-parser".equals(typeName) && componentClass == Parser.class) {
+ pendingDefaultParserConfig = configNode;
+ continue;
+ }
+ if ("default-detector".equals(typeName) && componentClass == Detector.class) {
+ pendingDefaultDetectorConfig = configNode;
+ continue;
+ }
+ if ("default-encoding-detector".equals(typeName) && componentClass == EncodingDetector.class) {
+ pendingDefaultEncodingDetectorConfig = configNode;
+ continue;
+ }
+
try {
// Create wrapper node: { "type-name": {...config...} }
ObjectNode wrapperNode = objectMapper.createObjectNode();
@@ -670,91 +731,127 @@ public class TikaLoader {
// ==================== Auto-Exclusion Post-Processing ====================
/**
- * Applies auto-exclusion to parsers: when both explicit parsers and DefaultParser
- * are configured, the explicit parser classes are automatically excluded from
- * DefaultParser's SPI loading to prevent duplicates.
+ * Creates DefaultParser (if configured) with config exclusions + auto-exclusions.
+ * Auto-exclusions are the explicit parser types to prevent duplicates.
+ * <p>
+ * Note: EncodingDetector and Renderer are configured later in configureParserDependencies.
*/
@SuppressWarnings("unchecked")
- private List<Parser> applyParserAutoExclusions(List<Parser> parsers) {
- // Find all explicitly configured parser types (not DefaultParser)
- Set<Class<?>> explicitTypes = new HashSet<>();
- for (Parser p : parsers) {
- if (!(p instanceof DefaultParser)) {
- explicitTypes.add(p.getClass());
- }
- }
-
- // If no explicit types, no auto-exclusions needed - but still preserve the list
- if (explicitTypes.isEmpty()) {
+ private List<Parser> applyParserAutoExclusions(List<Parser> parsers) throws IOException {
+ // If no DefaultParser was configured, just return the list
+ if (pendingDefaultParserConfig == null) {
return parsers;
}
- // Recreate any DefaultParser with explicit types as auto-exclusions
- List<Parser> adjusted = new ArrayList<>();
+ // Parse exclusions from config
+ Set<Class<? extends Parser>> exclusions = parseExclusions(pendingDefaultParserConfig, Parser.class);
+
+ // Add auto-exclusions (explicit parser types)
for (Parser p : parsers) {
- if (p instanceof DefaultParser dp) {
- // Combine explicit exclusions from config with auto-exclusions
- Set<Class<? extends Parser>> combinedExclusions = new HashSet<>();
-
- // Add config-specified exclusions
- combinedExclusions.addAll(dp.getExcludedClasses());
-
- // Add auto-exclusions (explicit parser types)
- combinedExclusions.addAll((Set<Class<? extends Parser>>) (Set<?>) explicitTypes);
-
- adjusted.add(new DefaultParser(
- getMediaTypeRegistry(),
- new ServiceLoader(classLoader),
- combinedExclusions));
- } else {
- adjusted.add(p);
- }
+ exclusions.add((Class<? extends Parser>) p.getClass());
}
- return adjusted;
+
+ // Create DefaultParser with all exclusions
+ List<Parser> result = new ArrayList<>(parsers);
+ result.add(new DefaultParser(
+ getMediaTypeRegistry(),
+ new ServiceLoader(classLoader),
+ exclusions));
+
+ pendingDefaultParserConfig = null;
+ return result;
}
/**
- * Applies auto-exclusion to detectors: when both explicit detectors and DefaultDetector
- * are configured, the explicit detector classes are automatically excluded from
- * DefaultDetector's SPI loading to prevent duplicates.
+ * Creates DefaultDetector (if configured) with config exclusions + auto-exclusions.
+ * Auto-exclusions are the explicit detector types to prevent duplicates.
*/
@SuppressWarnings("unchecked")
- private List<Detector> applyDetectorAutoExclusions(List<Detector> detectors) {
- // Find all explicitly configured detector types (not DefaultDetector)
- Set<Class<?>> explicitTypes = new HashSet<>();
+ private List<Detector> applyDetectorAutoExclusions(List<Detector> detectors) throws IOException {
+ // If no DefaultDetector was configured, just return the list
+ if (pendingDefaultDetectorConfig == null) {
+ return detectors;
+ }
+
+ // Parse exclusions from config
+ Set<Class<? extends Detector>> exclusions = parseExclusions(pendingDefaultDetectorConfig, Detector.class);
+
+ // Add auto-exclusions (explicit detector types)
for (Detector d : detectors) {
- if (!(d instanceof DefaultDetector)) {
- explicitTypes.add(d.getClass());
- }
+ exclusions.add((Class<? extends Detector>) d.getClass());
}
- // If no explicit types, no auto-exclusions needed - but still preserve the list
- if (explicitTypes.isEmpty()) {
- return detectors;
+ // Create DefaultDetector with all exclusions
+ List<Detector> result = new ArrayList<>(detectors);
+ result.add(new DefaultDetector(
+ getMimeTypes(),
+ new ServiceLoader(classLoader),
+ exclusions));
+
+ pendingDefaultDetectorConfig = null;
+ return result;
+ }
+
+ /**
+ * Creates DefaultEncodingDetector (if configured) with config exclusions + auto-exclusions.
+ * Auto-exclusions are the explicit encoding detector types to prevent duplicates.
+ */
+ @SuppressWarnings("unchecked")
+ private List<EncodingDetector> applyEncodingDetectorAutoExclusions(List<EncodingDetector> encodingDetectors)
+ throws IOException {
+ // If no DefaultEncodingDetector was configured, just return the list
+ if (pendingDefaultEncodingDetectorConfig == null) {
+ return encodingDetectors;
}
- // Recreate any DefaultDetector with explicit types as auto-exclusions
- List<Detector> adjusted = new ArrayList<>();
- for (Detector d : detectors) {
- if (d instanceof DefaultDetector dd) {
- // Combine explicit exclusions from config with auto-exclusions
- Set<Class<? extends Detector>> combinedExclusions = new HashSet<>();
-
- // Add config-specified exclusions
- combinedExclusions.addAll(dd.getExcludedClasses());
-
- // Add auto-exclusions (explicit detector types)
- combinedExclusions.addAll((Set<Class<? extends Detector>>) (Set<?>) explicitTypes);
-
- adjusted.add(new DefaultDetector(
- getMimeTypes(),
- new ServiceLoader(classLoader),
- combinedExclusions));
- } else {
- adjusted.add(d);
+ // Parse exclusions from config
+ Set<Class<? extends EncodingDetector>> exclusions =
+ parseExclusions(pendingDefaultEncodingDetectorConfig, EncodingDetector.class);
+
+ // Add auto-exclusions (explicit encoding detector types)
+ for (EncodingDetector ed : encodingDetectors) {
+ exclusions.add((Class<? extends EncodingDetector>) ed.getClass());
+ }
+
+ // Create DefaultEncodingDetector with all exclusions
+ List<EncodingDetector> result = new ArrayList<>(encodingDetectors);
+ result.add(new DefaultEncodingDetector(
+ new ServiceLoader(classLoader),
+ exclusions));
+
+ pendingDefaultEncodingDetectorConfig = null;
+ return result;
+ }
+
+ /**
+ * Parses exclusions from a config node.
+ * Supports both "exclude" and "_exclude" field names.
+ */
+ @SuppressWarnings("unchecked")
+ private <T> Set<Class<? extends T>> parseExclusions(JsonNode configNode, Class<T> componentClass)
+ throws IOException {
+ Set<Class<? extends T>> exclusions = new HashSet<>();
+
+ if (configNode == null || !configNode.isObject()) {
+ return exclusions;
+ }
+
+ JsonNode excludeNode = configNode.has("exclude") ?
+ configNode.get("exclude") : configNode.get("_exclude");
+
+ if (excludeNode != null && excludeNode.isArray()) {
+ for (JsonNode item : excludeNode) {
+ String typeName = item.asText();
+ try {
+ Class<?> clazz = ComponentNameResolver.resolveClass(typeName, classLoader);
+ exclusions.add((Class<? extends T>) clazz);
+ } catch (ClassNotFoundException e) {
+ throw new IOException("Unknown type in exclude list: " + typeName, e);
+ }
}
}
- return adjusted;
+
+ return exclusions;
}
// ==================== Serialization ====================
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
index 845cdec3c..14f0fda5f 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
@@ -60,9 +60,7 @@ import org.apache.tika.parser.DefaultParser;
import org.apache.tika.parser.Parser;
import org.apache.tika.parser.ParserDecorator;
import org.apache.tika.renderer.Renderer;
-import org.apache.tika.serialization.serdes.DefaultDetectorDeserializer;
import org.apache.tika.serialization.serdes.DefaultDetectorSerializer;
-import org.apache.tika.serialization.serdes.DefaultParserDeserializer;
import org.apache.tika.serialization.serdes.DefaultParserSerializer;
/**
@@ -277,23 +275,24 @@ public class TikaModule extends SimpleModule {
try {
Object instance;
- // Handle DefaultParser and DefaultDetector with dedicated deserializers
+ // DefaultParser and DefaultDetector must be loaded via TikaLoader for proper dependency injection
if (clazz == DefaultParser.class) {
- instance = deserializeWithNode(new DefaultParserDeserializer(), cleanedConfig, mapper);
+ throw new IOException("DefaultParser must be loaded via TikaLoader, not directly " +
+ "via Jackson deserialization. Use TikaLoader.load() to load configuration.");
} else if (clazz == DefaultDetector.class) {
- instance = deserializeWithNode(new DefaultDetectorDeserializer(), cleanedConfig, mapper);
+ throw new IOException("DefaultDetector must be loaded via TikaLoader, not directly " +
+ "via Jackson deserialization. Use TikaLoader.load() to load configuration.");
} else if (cleanedConfig == null || cleanedConfig.isEmpty()) {
// If no config, use default constructor
instance = clazz.getDeclaredConstructor().newInstance();
} else if (SelfConfiguring.class.isAssignableFrom(clazz)) {
- // SelfConfiguring components must use JsonConfig constructor
+ // SelfConfiguring components: prefer JsonConfig constructor if available
Constructor<?> jsonConfigCtor = findJsonConfigConstructor(clazz);
if (jsonConfigCtor != null) {
String json = mapper.writeValueAsString(cleanedConfig);
instance = jsonConfigCtor.newInstance((JsonConfig) () -> json);
} else {
- throw new IOException("SelfConfiguring component '" + typeName +
- "' must have a constructor that accepts JsonConfig, but none was found.");
+ instance = clazz.getDeclaredConstructor().newInstance();
}
} else {
// Non-SelfConfiguring: use Jackson bean deserialization
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorDeserializer.java
deleted file mode 100644
index 97ce76029..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorDeserializer.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.serialization.serdes;
-
-import java.util.Collection;
-
-import org.apache.tika.config.ServiceLoader;
-import org.apache.tika.detect.DefaultDetector;
-import org.apache.tika.detect.Detector;
-import org.apache.tika.mime.MimeTypes;
-
-/**
- * Deserializer for DefaultDetector that handles exclusions.
- * <p>
- * Supports JSON formats:
- * <pre>
- * "default-detector"
- * { "default-detector": {} }
- * { "default-detector": { "exclude": ["html-detector", "zip-detector"] } }
- * </pre>
- */
-public class DefaultDetectorDeserializer extends SpiCompositeDeserializer<DefaultDetector> {
-
- @Override
- @SuppressWarnings("unchecked")
- protected DefaultDetector createInstance(Collection<Class<?>> excludedClasses) {
- if (excludedClasses == null || excludedClasses.isEmpty()) {
- return new DefaultDetector();
- }
- return new DefaultDetector(MimeTypes.getDefaultMimeTypes(),
- new ServiceLoader(),
- (Collection<Class<? extends Detector>>) (Collection<?>) excludedClasses);
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserDeserializer.java
deleted file mode 100644
index ffc491e61..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserDeserializer.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.serialization.serdes;
-
-import java.util.Collection;
-
-import org.apache.tika.config.ServiceLoader;
-import org.apache.tika.mime.MediaTypeRegistry;
-import org.apache.tika.parser.DefaultParser;
-import org.apache.tika.parser.Parser;
-
-/**
- * Deserializer for DefaultParser that handles exclusions.
- * <p>
- * Supports JSON formats:
- * <pre>
- * "default-parser"
- * { "default-parser": {} }
- * { "default-parser": { "exclude": ["html-parser", "xml-parser"] } }
- * </pre>
- */
-public class DefaultParserDeserializer extends SpiCompositeDeserializer<DefaultParser> {
-
- @Override
- @SuppressWarnings("unchecked")
- protected DefaultParser createInstance(Collection<Class<?>> excludedClasses) {
- if (excludedClasses == null || excludedClasses.isEmpty()) {
- return new DefaultParser();
- }
- return new DefaultParser(MediaTypeRegistry.getDefaultRegistry(),
- new ServiceLoader(),
- (Collection<Class<? extends Parser>>) (Collection<?>) excludedClasses);
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeDeserializer.java
deleted file mode 100644
index 0218ca3d8..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeDeserializer.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.serialization.serdes;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
-
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-
-import org.apache.tika.serialization.ComponentNameResolver;
-
-/**
- * Abstract base deserializer for SPI-loaded composite types that support exclusions.
- * <p>
- * Handles JSON like:
- * <pre>
- * { "exclude": ["html-detector", "zip-detector"] }
- * </pre>
- * or simply an empty object:
- * <pre>
- * {}
- * </pre>
- * <p>
- * Note: The outer type wrapper (e.g., "default-detector") is handled by TikaModule.
- * This deserializer receives just the inner config object.
- * <p>
- * Subclasses implement {@link #createInstance(Collection)} to create the appropriate
- * composite type with the exclusions applied.
- *
- * @param <T> the composite type (e.g., DefaultDetector, DefaultParser)
- */
-public abstract class SpiCompositeDeserializer<T> extends JsonDeserializer<T> {
-
- @Override
- public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
- JsonNode node = p.readValueAsTree();
- Collection<Class<?>> excludedClasses = parseExclusions(node);
- return createInstance(excludedClasses);
- }
-
- /**
- * Parse exclusions from config node.
- * Supports both "exclude" and "_exclude" field names.
- */
- protected Collection<Class<?>> parseExclusions(JsonNode node) throws IOException {
- Set<Class<?>> excludedClasses = new HashSet<>();
-
- if (node == null || !node.isObject()) {
- return excludedClasses;
- }
-
- // Support both "exclude" and "_exclude" for compatibility
- JsonNode excludeNode = node.has("exclude") ? node.get("exclude") : node.get("_exclude");
-
- if (excludeNode != null && excludeNode.isArray()) {
- for (JsonNode item : excludeNode) {
- String typeName = item.asText();
- try {
- Class<?> clazz = ComponentNameResolver.resolveClass(typeName,
- Thread.currentThread().getContextClassLoader());
- excludedClasses.add(clazz);
- } catch (ClassNotFoundException e) {
- throw new IOException("Unknown type in exclude list: " + typeName, e);
- }
- }
- }
-
- return excludedClasses;
- }
-
- /**
- * Create an instance of the composite type with the specified exclusions.
- *
- * @param excludedClasses classes to exclude from SPI loading
- * @return the new instance
- */
- protected abstract T createInstance(Collection<Class<?>> excludedClasses);
-}
(tika) 01/05: TIKA-4585 -- simplify ParseContext serialization -WIP
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4585-simplify-serialization
in repository https://gitbox.apache.org/repos/asf/tika.git
commit 5de26e049356ec8870c07d1c0aa0bdd5c26474e2
Author: tallison <ta...@apache.org>
AuthorDate: Sat Dec 20 09:42:46 2025 -0500
TIKA-4585 -- simplify ParseContext serialization -WIP
---
.../org/apache/tika/config/ConfigContainer.java | 99 ----
.../org/apache/tika/config/ParseContextConfig.java | 44 +-
.../org/apache/tika/config/SelfConfiguring.java | 9 +-
.../org/apache/tika/config/TikaTaskTimeout.java | 3 +
.../org/apache/tika/detect/DefaultDetector.java | 16 +
.../apache/tika/parser/AutoDetectParserConfig.java | 6 +-
.../java/org/apache/tika/parser/DefaultParser.java | 19 +
.../java/org/apache/tika/parser/ParseContext.java | 135 +++++-
.../org/apache/tika/parser/ParserDecorator.java | 114 +++--
.../org/apache/tika/parser/RegexCaptureParser.java | 53 ++-
.../tika/parser/RegexCaptureParserConfig.java | 60 +++
.../tika/parser/external2/ExternalParser.java | 145 ++----
.../parser/external2/ExternalParserConfig.java | 110 +++++
.../apache/tika/config/ParseContextConfigTest.java | 18 +-
.../apache/tika/parser/RegexCaptureParserTest.java | 7 +-
.../apache/tika/pipes/grpc/TikaGrpcServerImpl.java | 5 +-
.../apache/tika/parser/ocr/TesseractOCRParser.java | 9 +-
.../java/org/apache/tika/parser/pdf/PDFParser.java | 2 +-
.../tika/parser/ocr/TesseractOCRParserTest.java | 23 +-
.../org/apache/tika/parser/pdf/PDFParserTest.java | 6 +-
.../serialization/FetchEmitTupleDeserializer.java | 6 +-
.../serialization/FetchEmitTupleSerializer.java | 2 +-
.../pipes/core/serialization/JsonEmitData.java | 4 +-
.../core/serialization/JsonFetchEmitTuple.java | 6 +-
.../core/serialization/JsonFetchEmitTupleList.java | 6 +-
.../apache/tika/pipes/core/PipesClientTest.java | 13 +-
.../tika/pipes/emitter/fs/FileSystemEmitter.java | 18 +-
.../fs/FileSystemEmitterRuntimeConfigTest.java | 29 +-
.../tika/pipes/fetcher/http/HttpFetcher.java | 18 +-
.../tika/pipes/fetcher/http/HttpFetcherTest.java | 5 +-
.../apache/tika/config/loader/ComponentInfo.java | 2 +-
.../config/loader/CompositeComponentLoader.java | 168 -------
.../apache/tika/config/loader/DetectorLoader.java | 197 --------
.../tika/config/loader/EncodingDetectorLoader.java | 189 --------
.../apache/tika/config/loader/ParserLoader.java | 311 -------------
.../org/apache/tika/config/loader/TikaLoader.java | 518 +++++++++++++++++----
.../config/loader/TikaObjectMapperFactory.java | 10 +-
.../tika/config/loader/TranslatorLoader.java | 117 -----
.../apache/tika/serialization/ComponentConfig.java | 153 ++++++
.../tika/serialization/ComponentNameResolver.java | 82 ++++
.../tika/serialization/ConfigDeserializer.java | 68 +--
.../apache/tika/serialization/JsonMetadata.java | 2 +
.../tika/serialization/JsonMetadataList.java | 2 +
.../serialization/ParseContextDeserializer.java | 185 --------
.../tika/serialization/ParseContextSerializer.java | 133 ------
.../tika/serialization/ParseContextUtils.java | 55 +--
.../org/apache/tika/serialization/TikaModule.java | 503 ++++++++++++++++++++
.../serdes/DefaultDetectorDeserializer.java | 48 ++
.../serdes/DefaultDetectorSerializer.java | 50 ++
.../serdes/DefaultParserDeserializer.java | 48 ++
.../serdes/DefaultParserSerializer.java | 50 ++
.../{ => serdes}/MetadataDeserializer.java | 2 +-
.../{ => serdes}/MetadataSerializer.java | 2 +-
.../serdes/ParseContextDeserializer.java | 93 ++++
.../serdes/ParseContextSerializer.java | 62 +++
.../serdes/SpiCompositeDeserializer.java | 97 ++++
.../serdes/SpiCompositeSerializer.java | 80 ++++
.../config/loader/TikaLoaderRoundTripTest.java | 294 ++++++++++++
.../apache/tika/config/loader/TikaLoaderTest.java | 79 +---
.../apache/tika/digest/MockDigesterFactory.java | 45 ++
.../metadata/filter/MetadataListFilterTest.java | 2 +-
.../tika/metadata/filter/TestMetadataFilter.java | 18 +-
.../writefilter/StandardWriteFilterTest.java | 6 +-
.../tika/parser/external2/ExternalParserTest.java | 4 +-
.../apache/tika/sax/UppercasingContentHandler.java | 104 +++++
.../tika/sax/UppercasingContentHandlerFactory.java | 52 +++
.../tika/serialization/ConfigDeserializerTest.java | 46 +-
.../CustomClassSerializationTest.java | 84 ++--
.../serialization/RoundTripSerializationTest.java | 281 +++++++++++
.../TestParseContextSerialization.java | 150 ++----
.../src/test/resources/configs/TIKA-3695.json | 3 +-
.../tika/server/core/resource/TikaResource.java | 2 +-
72 files changed, 3254 insertions(+), 2133 deletions(-)
diff --git a/tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java b/tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java
deleted file mode 100644
index 4ac320833..000000000
--- a/tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.config;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * This is effectively a copy of ParseContext that is to be used when serialization
- * intervenes between the caller and the processor as in tika-pipes, and elsewhere.
- *
- * The goal of this is to delegate deserialization to the consumers/receivers.
- * <p>
- * Stores configuration as named JSON strings, allowing parsers, fetchers, emitters,
- * and other components to look up their config by friendly name (e.g., "pdf-parser",
- * "fs-fetcher-1") and deserialize it on-demand.
- */
-public class ConfigContainer implements Serializable {
-
- private static final long serialVersionUID = 1L;
-
- private final Map<String, String> configs = new HashMap<>();
-
- public <T> void set(Class<T> key, String value) {
- if (value != null) {
- String keyName = key.getName();
- if (configs.containsKey(keyName)) {
- throw new IllegalArgumentException(
- "Config key '" + keyName + "' already exists. Cannot overwrite existing configuration.");
- }
- configs.put(keyName, value);
- }
- }
-
- public void set(String name, String value) {
- if (value != null) {
- if (configs.containsKey(name)) {
- throw new IllegalArgumentException(
- "Config key '" + name + "' already exists. Cannot overwrite existing configuration.");
- }
- configs.put(name, value);
- }
- }
-
- public <T> Optional<JsonConfig> get(Class<T> key) {
- String json = configs.get(key.getName());
- return json == null ? Optional.empty() : Optional.of(() -> json);
- }
-
- public Optional<JsonConfig> get(String key) {
- String json = configs.get(key);
- return json == null ? Optional.empty() : Optional.of(() -> json);
- }
-
- public JsonConfig get(String key, String defaultMissing) {
- String val = configs.get(key);
- if (val == null) {
- val = defaultMissing;
- }
- final String jsonValue = val;
- return () -> jsonValue;
- }
-
- public Set<String> getKeys() {
- return Collections.unmodifiableSet(configs.keySet());
- }
-
- public boolean isEmpty() {
- return configs.isEmpty();
- }
-
- /**
- * Removes the configuration with the specified key.
- *
- * @param key the friendly name of the config to remove
- * @return the previous value associated with the key, or null if there was no mapping
- */
- public String remove(String key) {
- return configs.remove(key);
- }
-}
diff --git a/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java b/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java
index 888c9e515..8f1dc0337 100644
--- a/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java
+++ b/tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java
@@ -24,13 +24,13 @@ import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.parser.ParseContext;
/**
- * Facade for accessing runtime configuration from ParseContext's ConfigContainer.
+ * Facade for accessing runtime configuration from ParseContext's jsonConfigs.
* <p>
* This wrapper provides a safe way for parsers to access runtime configuration
* without directly depending on tika-serialization. It performs these critical checks:
* <ul>
- * <li>If ConfigContainer has config for the requested key but ConfigDeserializer
- * is not on the classpath, throws IllegalStateException with a clear error message</li>
+ * <li>If ParseContext has JSON config for the requested key but ConfigDeserializer
+ * is not on the classpath, throws TikaConfigException with a clear error message</li>
* <li>If ConfigDeserializer is available, delegates to it for deserialization</li>
* <li>If no config is present, returns the default config</li>
* </ul>
@@ -55,7 +55,7 @@ public class ParseContextConfig {
Method hasMethod = null;
try {
clazz = Class.forName("org.apache.tika.serialization.ConfigDeserializer");
- getMethod = clazz.getMethod("getConfig",
+ getMethod = clazz.getMethod("getConfig",
ParseContext.class, String.class, Class.class, Object.class);
hasMethod = clazz.getMethod("hasConfig", ParseContext.class, String.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
@@ -74,10 +74,11 @@ public class ParseContextConfig {
* re-deserializing. This is efficient for embedded documents where the config
* was already deserialized for the parent document.
* <p>
- * If not found, it checks ConfigContainer for the config key and deserializes
- * the JSON. The deserialized config is also set in ParseContext for future lookups.
+ * If not found, it checks jsonConfigs for the config key and deserializes
+ * the JSON. The deserialized config is cached in resolvedConfigs and also
+ * set in the main ParseContext for future lookups.
* <p>
- * This method performs defensive checking: if the ConfigContainer has configuration
+ * This method performs defensive checking: if the ParseContext has JSON configuration
* for the requested key but the ConfigDeserializer is not available on the classpath,
* it throws TikaConfigException to prevent silent failures.
*
@@ -87,7 +88,7 @@ public class ParseContextConfig {
* @param defaultConfig the default configuration to use if no runtime config exists
* @param <T> the configuration type
* @return the runtime config merged with defaults, or the default config if no runtime config
- * @throws TikaConfigException if ConfigContainer has config but ConfigDeserializer is not on classpath
+ * @throws TikaConfigException if ParseContext has JSON config but ConfigDeserializer is not on classpath
* @throws IOException if deserialization fails
*/
public static <T> T getConfig(ParseContext context, String configKey,
@@ -104,25 +105,18 @@ public class ParseContextConfig {
return existingConfig;
}
- ConfigContainer configContainer = context.get(ConfigContainer.class);
- if (configContainer == null) {
+ // Check for JSON config
+ if (!context.hasJsonConfig(configKey)) {
return defaultConfig;
}
- // Check if there's config for this specific key
- boolean hasConfigForKey = configContainer.get(configKey).isPresent();
- if (!hasConfigForKey) {
- return defaultConfig;
- }
-
- // Config exists for this key - ConfigDeserializer MUST be available
+ // JSON config exists for this key - ConfigDeserializer MUST be available
if (CONFIG_DESERIALIZER_CLASS == null) {
throw new TikaConfigException(String.format(Locale.ROOT,
- "ParseContext contains ConfigContainer with configuration for '%s' " +
+ "ParseContext contains JSON configuration for '%s' " +
"but org.apache.tika.serialization.ConfigDeserializer is not on the classpath. " +
"This means your runtime configuration will be ignored. " +
- "To fix: add tika-serialization as a dependency, or remove the ConfigContainer " +
- "from ParseContext if runtime configuration via ConfigContainer is not needed.",
+ "To fix: add tika-serialization as a dependency.",
configKey));
}
@@ -150,19 +144,13 @@ public class ParseContextConfig {
*
* @param context the parse context
* @param configKey the configuration key
- * @return true if config exists for this key
+ * @return true if JSON config exists for this key
*/
public static boolean hasConfig(ParseContext context, String configKey) {
if (context == null) {
return false;
}
-
- ConfigContainer configContainer = context.get(ConfigContainer.class);
- if (configContainer == null) {
- return false;
- }
-
- return configContainer.get(configKey).isPresent();
+ return context.hasJsonConfig(configKey);
}
/**
diff --git a/tika-core/src/main/java/org/apache/tika/config/SelfConfiguring.java b/tika-core/src/main/java/org/apache/tika/config/SelfConfiguring.java
index 3f43b935b..06177f830 100644
--- a/tika-core/src/main/java/org/apache/tika/config/SelfConfiguring.java
+++ b/tika-core/src/main/java/org/apache/tika/config/SelfConfiguring.java
@@ -18,12 +18,12 @@ package org.apache.tika.config;
/**
* Marker interface indicating that a component reads its own configuration
- * from {@link ConfigContainer} inside the {@link org.apache.tika.parser.ParseContext} at runtime.
+ * from {@link org.apache.tika.parser.ParseContext}'s jsonConfigs at runtime.
* <p>
* Components implementing this interface will NOT be automatically resolved
* by ParseContextUtils. Instead, the JSON configuration will remain in
- * ConfigContainer, and the component is responsible for reading and applying
- * its own configuration during execution.
+ * ParseContext's jsonConfigs, and the component is responsible for reading
+ * and applying its own configuration during execution.
* <p>
* This is typically used by parsers and other components that need fine-grained
* control over how their configuration is loaded and merged with defaults.
@@ -36,7 +36,7 @@ package org.apache.tika.config;
* private final PDFParserConfig defaultConfig;
*
* public void parse(..., ParseContext context) {
- * // Component reads its own config from ConfigContainer
+ * // Component reads its own config from ParseContext
* PDFParserConfig config = ParseContextConfig.getConfig(
* context, "pdf-parser", PDFParserConfig.class, defaultConfig);
* // use config...
@@ -48,7 +48,6 @@ package org.apache.tika.config;
* automatically deserialized and added to ParseContext by ParseContextUtils.
*
* @since Apache Tika 4.0
- * @see ConfigContainer
* @see ParseContextConfig
*/
public interface SelfConfiguring {
diff --git a/tika-core/src/main/java/org/apache/tika/config/TikaTaskTimeout.java b/tika-core/src/main/java/org/apache/tika/config/TikaTaskTimeout.java
index cf3928fca..2f922c52a 100644
--- a/tika-core/src/main/java/org/apache/tika/config/TikaTaskTimeout.java
+++ b/tika-core/src/main/java/org/apache/tika/config/TikaTaskTimeout.java
@@ -22,6 +22,9 @@ import org.apache.tika.parser.ParseContext;
/**
* Configuration class for specifying parse task timeout.
+ * <p>
+ * This is a config POJO (not a component like Parser/Detector), so it uses
+ * standard Jackson format rather than compact component format:
* <pre>
* {
* "parse-context": {
diff --git a/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java b/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java
index 038d274e4..2d71c5b18 100644
--- a/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java
+++ b/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java
@@ -16,12 +16,14 @@
*/
package org.apache.tika.detect;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.imageio.spi.ServiceRegistry;
import org.apache.tika.config.ServiceLoader;
+import org.apache.tika.config.TikaComponent;
import org.apache.tika.mime.MimeTypes;
import org.apache.tika.utils.ServiceLoaderUtils;
@@ -37,6 +39,7 @@ import org.apache.tika.utils.ServiceLoaderUtils;
*
* @since Apache Tika 0.9
*/
+@TikaComponent(spi = false)
public class DefaultDetector extends CompositeDetector {
/**
@@ -44,11 +47,15 @@ public class DefaultDetector extends CompositeDetector {
*/
private static final long serialVersionUID = -8170114575326908027L;
private transient final ServiceLoader loader;
+ private final Collection<Class<? extends Detector>> excludedClasses;
public DefaultDetector(MimeTypes types, ServiceLoader loader,
Collection<Class<? extends Detector>> excludeDetectors) {
super(types.getMediaTypeRegistry(), getDefaultDetectors(types, loader, excludeDetectors));
this.loader = loader;
+ this.excludedClasses = excludeDetectors != null ?
+ Collections.unmodifiableCollection(new ArrayList<>(excludeDetectors)) :
+ Collections.emptySet();
}
public DefaultDetector(MimeTypes types, ServiceLoader loader) {
@@ -124,4 +131,13 @@ public class DefaultDetector extends CompositeDetector {
}
}
+ /**
+ * Returns the classes that were explicitly excluded when constructing this detector.
+ * Used for round-trip serialization to preserve exclusion configuration.
+ *
+ * @return unmodifiable collection of excluded detector classes, never null
+ */
+ public Collection<Class<? extends Detector>> getExcludedClasses() {
+ return excludedClasses;
+ }
}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/AutoDetectParserConfig.java b/tika-core/src/main/java/org/apache/tika/parser/AutoDetectParserConfig.java
index 59a5fa96d..75ba7f6f7 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/AutoDetectParserConfig.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/AutoDetectParserConfig.java
@@ -33,8 +33,12 @@ import org.apache.tika.sax.ContentHandlerDecoratorFactory;
* when parsing data that is extremely compressible and resembles a ZIP
* bomb. Null values will be ignored and will not affect the default values
* in SecureContentHandler.
+ * <p>
+ * This is a config POJO. It uses standard Jackson deserialization for its
+ * primitive fields, but component fields (like embeddedDocumentExtractorFactory)
+ * use compact format.
*/
-@TikaComponent
+@TikaComponent(spi = false)
public class AutoDetectParserConfig implements Serializable {
private static ContentHandlerDecoratorFactory NOOP_CONTENT_HANDLER_DECORATOR_FACTORY =
diff --git a/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java b/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java
index 3205ea81d..d6a0c945e 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import org.apache.tika.config.ServiceLoader;
+import org.apache.tika.config.TikaComponent;
import org.apache.tika.detect.DefaultEncodingDetector;
import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.mime.MediaType;
@@ -38,6 +39,7 @@ import org.apache.tika.utils.ServiceLoaderUtils;
*
* @since Apache Tika 0.8
*/
+@TikaComponent(spi = false)
public class DefaultParser extends CompositeParser {
/**
@@ -45,12 +47,16 @@ public class DefaultParser extends CompositeParser {
*/
private static final long serialVersionUID = 3612324825403757520L;
private transient final ServiceLoader loader;
+ private final Collection<Class<? extends Parser>> excludedClasses;
public DefaultParser(MediaTypeRegistry registry, ServiceLoader loader,
Collection<Class<? extends Parser>> excludeParsers,
EncodingDetector encodingDetector, Renderer renderer) {
super(registry, getDefaultParsers(loader, encodingDetector, renderer, excludeParsers));
this.loader = loader;
+ this.excludedClasses = excludeParsers != null ?
+ Collections.unmodifiableCollection(new ArrayList<>(excludeParsers)) :
+ Collections.emptySet();
}
public DefaultParser(MediaTypeRegistry registry, ServiceLoader loader,
@@ -59,6 +65,9 @@ public class DefaultParser extends CompositeParser {
getDefaultParsers(loader, new DefaultEncodingDetector(loader),
new CompositeRenderer(loader), excludeParsers));
this.loader = loader;
+ this.excludedClasses = excludeParsers != null ?
+ Collections.unmodifiableCollection(new ArrayList<>(excludeParsers)) :
+ Collections.emptySet();
}
public DefaultParser(MediaTypeRegistry registry, ServiceLoader loader,
@@ -175,4 +184,14 @@ public class DefaultParser extends CompositeParser {
}
return parsers;
}
+
+ /**
+ * Returns the classes that were explicitly excluded when constructing this parser.
+ * Used for round-trip serialization to preserve exclusion configuration.
+ *
+ * @return unmodifiable collection of excluded parser classes, never null
+ */
+ public Collection<Class<? extends Parser>> getExcludedClasses() {
+ return excludedClasses;
+ }
}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java b/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
index 6e16391c2..db4a0e157 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/ParseContext.java
@@ -21,7 +21,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import org.apache.tika.config.ConfigContainer;
+import org.apache.tika.config.JsonConfig;
/**
* Parse context. Used to pass context information to Tika parsers.
@@ -37,10 +37,23 @@ public class ParseContext implements Serializable {
private static final long serialVersionUID = -5921436862145826534L;
/**
- * Map of objects in this context
+ * Map of typed objects in this context, keyed by class name.
*/
private final Map<String, Object> context = new HashMap<>();
+ /**
+ * Map of JSON configs, keyed by component name (e.g., "pdf-parser").
+ * This is the source of truth for round-trip serialization.
+ * Using JsonConfig interface allows for future extension with metadata.
+ */
+ private final Map<String, JsonConfig> jsonConfigs = new HashMap<>();
+
+ /**
+ * Cache of resolved objects from jsonConfigs, keyed by component name.
+ * This is ignored during serialization to preserve round-trip fidelity.
+ */
+ private final transient Map<String, Object> resolvedConfigs = new HashMap<>();
+
/**
* Adds the given value to the context as an implementation of the given
* interface.
@@ -87,33 +100,109 @@ public class ParseContext implements Serializable {
}
/**
- * Adds a configuration by friendly name for serialization.
+ * Sets a JSON configuration by component name.
* <p>
- * This is a convenience method for adding configs that will be serialized
- * and resolved at runtime. The config is stored in a {@link ConfigContainer}
- * and will be resolved to an actual object via the component registry.
+ * This stores the JSON config for later resolution. The JSON will be
+ * deserialized when requested via the component registry in tika-serialization.
* <p>
* Example:
* <pre>
- * parseContext.addConfig("tika-task-timeout", "{\"timeoutMillis\": 5000}");
- * parseContext.addConfig("handler-config", "{\"type\": \"XML\", \"parseMode\": \"RMETA\"}");
+ * parseContext.setJsonConfig("pdf-parser", () -> "{\"ocrStrategy\": \"AUTO\"}");
+ * parseContext.setJsonConfig("handler-config", () -> "{\"type\": \"XML\"}");
* </pre>
*
- * @param key the friendly name of the config (e.g., "tika-task-timeout", "handler-config")
+ * @param name the component name (e.g., "pdf-parser", "handler-config")
+ * @param config the JSON configuration
+ * @since Apache Tika 4.0
+ */
+ public void setJsonConfig(String name, JsonConfig config) {
+ if (config != null) {
+ jsonConfigs.put(name, config);
+ } else {
+ jsonConfigs.remove(name);
+ resolvedConfigs.remove(name);
+ }
+ }
+
+ /**
+ * Sets a JSON configuration by component name using a raw JSON string.
+ * <p>
+ * Convenience method that wraps the string in a JsonConfig.
+ *
+ * @param name the component name (e.g., "pdf-parser", "handler-config")
* @param json the JSON configuration string
* @since Apache Tika 4.0
*/
- public void addConfig(String key, String json) {
- ConfigContainer container = get(ConfigContainer.class);
- if (container == null) {
- container = new ConfigContainer();
- set(ConfigContainer.class, container);
+ public void setJsonConfig(String name, String json) {
+ setJsonConfig(name, json != null ? () -> json : null);
+ }
+
+ /**
+ * Gets a JSON configuration by component name.
+ *
+ * @param name the component name
+ * @return the JsonConfig, or null if not found
+ * @since Apache Tika 4.0
+ */
+ public JsonConfig getJsonConfig(String name) {
+ return jsonConfigs.get(name);
+ }
+
+ /**
+ * Returns all JSON configurations for serialization.
+ *
+ * @return unmodifiable map of component name to JsonConfig
+ * @since Apache Tika 4.0
+ */
+ public Map<String, JsonConfig> getJsonConfigs() {
+ return Collections.unmodifiableMap(jsonConfigs);
+ }
+
+ /**
+ * Gets a resolved configuration object from the cache.
+ * <p>
+ * This is used by tika-serialization after deserializing a JSON config.
+ * The resolved object is cached here to avoid repeated deserialization.
+ *
+ * @param name the component name
+ * @return the resolved object, or null if not cached
+ * @since Apache Tika 4.0
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T getResolvedConfig(String name) {
+ return (T) resolvedConfigs.get(name);
+ }
+
+ /**
+ * Caches a resolved configuration object.
+ * <p>
+ * Called by tika-serialization after deserializing a JSON config.
+ *
+ * @param name the component name
+ * @param config the resolved configuration object
+ * @since Apache Tika 4.0
+ */
+ public void setResolvedConfig(String name, Object config) {
+ if (config != null) {
+ resolvedConfigs.put(name, config);
+ } else {
+ resolvedConfigs.remove(name);
}
- container.set(key, json);
+ }
+
+ /**
+ * Checks if a JSON configuration exists for the given component name.
+ *
+ * @param name the component name
+ * @return true if a JSON config exists
+ * @since Apache Tika 4.0
+ */
+ public boolean hasJsonConfig(String name) {
+ return jsonConfigs.containsKey(name);
}
public boolean isEmpty() {
- return context.isEmpty();
+ return context.isEmpty() && jsonConfigs.isEmpty();
}
@@ -141,16 +230,24 @@ public class ParseContext implements Serializable {
}
ParseContext that = (ParseContext) o;
- return context.equals(that.context);
+ if (!context.equals(that.context)) {
+ return false;
+ }
+ return jsonConfigs.equals(that.jsonConfigs);
}
@Override
public int hashCode() {
- return context.hashCode();
+ int result = context.hashCode();
+ result = 31 * result + jsonConfigs.hashCode();
+ return result;
}
@Override
public String toString() {
- return "ParseContext{" + "context=" + context + '}';
+ return "ParseContext{" +
+ "context=" + context +
+ ", jsonConfigs=" + jsonConfigs +
+ '}';
}
}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/ParserDecorator.java b/tika-core/src/main/java/org/apache/tika/parser/ParserDecorator.java
index 7438c99ac..63bc98402 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/ParserDecorator.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/ParserDecorator.java
@@ -18,6 +18,7 @@ package org.apache.tika.parser;
import java.io.IOException;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -68,20 +69,8 @@ public class ParserDecorator implements Parser {
* @param types supported media types
* @return the decorated parser
*/
- public static final Parser withTypes(Parser parser, final Set<MediaType> types) {
- return new ParserDecorator(parser) {
- private static final long serialVersionUID = -7345051519565330731L;
-
- @Override
- public Set<MediaType> getSupportedTypes(ParseContext context) {
- return types;
- }
-
- @Override
- public String getDecorationName() {
- return "With Types";
- }
- };
+ public static Parser withTypes(Parser parser, Set<MediaType> types) {
+ return new MimeFilteringDecorator(parser, types, null);
}
/**
@@ -92,26 +81,26 @@ public class ParserDecorator implements Parser {
* @param excludeTypes excluded/ignored media types
* @return the decorated parser
*/
- public static final Parser withoutTypes(Parser parser, final Set<MediaType> excludeTypes) {
- return new ParserDecorator(parser) {
- private static final long serialVersionUID = 7979614774021768609L;
-
- @Override
- public Set<MediaType> getSupportedTypes(ParseContext context) {
- // Get our own, writable copy of the types the parser supports
- Set<MediaType> parserTypes =
- new HashSet<>(super.getSupportedTypes(context));
- // Remove anything on our excludes list
- parserTypes.removeAll(excludeTypes);
- // Return whatever is left
- return parserTypes;
- }
+ public static Parser withoutTypes(Parser parser, Set<MediaType> excludeTypes) {
+ return new MimeFilteringDecorator(parser, null, excludeTypes);
+ }
- @Override
- public String getDecorationName() {
- return "Without Types";
- }
- };
+ /**
+ * Decorates the given parser with mime type filtering.
+ * Supports both include and exclude lists for round-trip serialization.
+ *
+ * @param parser the parser to be decorated
+ * @param includeTypes types to include (if non-empty, only these types are supported)
+ * @param excludeTypes types to exclude (removed from supported types)
+ * @return the decorated parser, or the original parser if no filtering needed
+ */
+ public static Parser withMimeFilters(Parser parser, Set<MediaType> includeTypes,
+ Set<MediaType> excludeTypes) {
+ if ((includeTypes == null || includeTypes.isEmpty()) &&
+ (excludeTypes == null || excludeTypes.isEmpty())) {
+ return parser;
+ }
+ return new MimeFilteringDecorator(parser, includeTypes, excludeTypes);
}
/**
@@ -134,6 +123,65 @@ public class ParserDecorator implements Parser {
return withTypes(p, types);
}
+ /**
+ * A ParserDecorator that filters supported mime types.
+ * Stores include/exclude sets for round-trip serialization.
+ * Results are cached when includeTypes is specified (deterministic case).
+ */
+ public static class MimeFilteringDecorator extends ParserDecorator {
+ private static final long serialVersionUID = 1L;
+
+ private final Set<MediaType> includeTypes;
+ private final Set<MediaType> excludeTypes;
+ private volatile Set<MediaType> cachedTypes;
+
+ public MimeFilteringDecorator(Parser parser, Set<MediaType> includeTypes,
+ Set<MediaType> excludeTypes) {
+ super(parser);
+ this.includeTypes = includeTypes != null ?
+ Collections.unmodifiableSet(new HashSet<>(includeTypes)) : Collections.emptySet();
+ this.excludeTypes = excludeTypes != null ?
+ Collections.unmodifiableSet(new HashSet<>(excludeTypes)) : Collections.emptySet();
+ }
+
+ @Override
+ public Set<MediaType> getSupportedTypes(ParseContext context) {
+ // If includeTypes is specified, result is deterministic - use cache
+ if (!includeTypes.isEmpty()) {
+ if (cachedTypes == null) {
+ Set<MediaType> types = new HashSet<>(includeTypes);
+ types.removeAll(excludeTypes);
+ cachedTypes = Collections.unmodifiableSet(types);
+ }
+ return cachedTypes;
+ }
+
+ // Otherwise compute from wrapped parser (may vary by context)
+ Set<MediaType> types = new HashSet<>(super.getSupportedTypes(context));
+ types.removeAll(excludeTypes);
+ return types;
+ }
+
+ @Override
+ public String getDecorationName() {
+ return "Mime Filtering";
+ }
+
+ /**
+ * @return the included mime types, or empty set if no include filter
+ */
+ public Set<MediaType> getIncludeTypes() {
+ return includeTypes;
+ }
+
+ /**
+ * @return the excluded mime types, or empty set if no exclude filter
+ */
+ public Set<MediaType> getExcludeTypes() {
+ return excludeTypes;
+ }
+ }
+
/**
* Delegates the method call to the decorated parser. Subclasses should
* override this method (and use <code>super.getSupportedTypes()</code>
diff --git a/tika-core/src/main/java/org/apache/tika/parser/RegexCaptureParser.java b/tika-core/src/main/java/org/apache/tika/parser/RegexCaptureParser.java
index 28b043148..00b835501 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/RegexCaptureParser.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/RegexCaptureParser.java
@@ -31,6 +31,8 @@ import java.util.regex.Pattern;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.TikaComponent;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
@@ -43,15 +45,40 @@ public class RegexCaptureParser implements Parser {
private static final Set<MediaType> SUPPORTED_TYPES =
Collections.singleton(MediaType.TEXT_PLAIN);
- private Map<String, Pattern> captureMap = new HashMap<>();
- private Map<String, Pattern> matchMap = new HashMap<>();
+ private final RegexCaptureParserConfig config;
+ private final Map<String, Pattern> captureMap;
+ private final Map<String, Pattern> matchMap;
+ private final boolean writeContent;
+
+ public RegexCaptureParser() {
+ this(new RegexCaptureParserConfig());
+ }
+
+ public RegexCaptureParser(RegexCaptureParserConfig config) {
+ this.config = config;
+ this.captureMap = new HashMap<>();
+ for (Map.Entry<String, String> e : config.getCaptureMap().entrySet()) {
+ this.captureMap.put(e.getKey(), Pattern.compile(e.getValue()));
+ }
+ this.matchMap = new HashMap<>();
+ for (Map.Entry<String, String> e : config.getMatchMap().entrySet()) {
+ this.matchMap.put(e.getKey(), Pattern.compile(e.getValue()));
+ }
+ this.writeContent = config.isWriteContent();
+ }
+
+ public RegexCaptureParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig, RegexCaptureParserConfig.class));
+ }
@Override
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
}
- private boolean writeContent = false;
+ public RegexCaptureParserConfig getConfig() {
+ return config;
+ }
@Override
public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata,
@@ -100,24 +127,4 @@ public class RegexCaptureParser implements Parser {
}
}
}
-
- public void setCaptureMap(Map<String, String> map) {
- for (Map.Entry<String, String> e : map.entrySet()) {
- String field = e.getKey();
- Pattern pattern = Pattern.compile(e.getValue());
- captureMap.put(field, pattern);
- }
- }
-
- public void setMatchMap(Map<String, String> map) {
- for (Map.Entry<String, String> e : map.entrySet()) {
- String field = e.getKey();
- Pattern pattern = Pattern.compile(e.getValue());
- matchMap.put(field, pattern);
- }
- }
-
- public void setWriteContent(boolean writeContent) {
- this.writeContent = writeContent;
- }
}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/RegexCaptureParserConfig.java b/tika-core/src/main/java/org/apache/tika/parser/RegexCaptureParserConfig.java
new file mode 100644
index 000000000..a9d299fb9
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/parser/RegexCaptureParserConfig.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Configuration for {@link RegexCaptureParser}.
+ */
+public class RegexCaptureParserConfig implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private Map<String, String> captureMap = new HashMap<>();
+ private Map<String, String> matchMap = new HashMap<>();
+ private boolean writeContent = false;
+
+ public RegexCaptureParserConfig() {
+ }
+
+ public Map<String, String> getCaptureMap() {
+ return captureMap;
+ }
+
+ public void setCaptureMap(Map<String, String> captureMap) {
+ this.captureMap = captureMap;
+ }
+
+ public Map<String, String> getMatchMap() {
+ return matchMap;
+ }
+
+ public void setMatchMap(Map<String, String> matchMap) {
+ this.matchMap = matchMap;
+ }
+
+ public boolean isWriteContent() {
+ return writeContent;
+ }
+
+ public void setWriteContent(boolean writeContent) {
+ this.writeContent = writeContent;
+ }
+}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/external2/ExternalParser.java b/tika-core/src/main/java/org/apache/tika/parser/external2/ExternalParser.java
index b44b7a9f3..536da8dd4 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/external2/ExternalParser.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/external2/ExternalParser.java
@@ -33,9 +33,10 @@ import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.TikaComponent;
import org.apache.tika.config.TikaTaskTimeout;
-import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
@@ -72,21 +73,40 @@ public class ExternalParser implements Parser {
private static final Logger LOG = LoggerFactory.getLogger(ExternalParser.class);
- private Set<MediaType> supportedTypes = new HashSet<>();
+ private final ExternalParserConfig config;
- private List<String> commandLine = new ArrayList<>();
+ // Cached values derived from config
+ private final Set<MediaType> supportedTypes;
+ private final List<String> commandLine;
+ private final Parser outputParser;
- private Parser outputParser = EmptyParser.INSTANCE;
-
- private boolean returnStdout = false;
-
- private boolean returnStderr = true;
-
- private long timeoutMs = DEFAULT_TIMEOUT_MS;
+ /**
+ * Default constructor - not typically useful since ExternalParser requires configuration.
+ */
+ public ExternalParser() {
+ this(new ExternalParserConfig());
+ }
- private int maxStdErr = 10000;
+ /**
+ * Programmatic constructor with typed config.
+ */
+ public ExternalParser(ExternalParserConfig config) {
+ this.config = config;
+ this.supportedTypes = new HashSet<>();
+ for (String s : config.getSupportedTypes()) {
+ this.supportedTypes.add(MediaType.parse(s));
+ }
+ this.commandLine = new ArrayList<>(config.getCommandLine());
+ this.outputParser = config.getOutputParser() != null ?
+ config.getOutputParser() : EmptyParser.INSTANCE;
+ }
- private int maxStdOut = 10000;
+ /**
+ * JSON config constructor - used for deserialization.
+ */
+ public ExternalParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig, ExternalParserConfig.class));
+ }
@Override
public Set<MediaType> getSupportedTypes(ParseContext context) {
@@ -120,14 +140,14 @@ public class ExternalParser implements Parser {
}
}
FileProcessResult result = null;
- long localTimeoutMillis = TikaTaskTimeout.getTimeoutMillis(context, timeoutMs);
+ long localTimeoutMillis = TikaTaskTimeout.getTimeoutMillis(context, config.getTimeoutMs());
if (outputFileInCommandline) {
result = ProcessUtils.execute(new ProcessBuilder(thisCommandLine),
- localTimeoutMillis, maxStdOut, maxStdErr);
+ localTimeoutMillis, config.getMaxStdOut(), config.getMaxStdErr());
} else {
outFile = Files.createTempFile("tika-external2-", "");
result = ProcessUtils.execute(new ProcessBuilder(thisCommandLine),
- localTimeoutMillis, outFile, maxStdErr);
+ localTimeoutMillis, outFile, config.getMaxStdErr());
}
metadata.set(ExternalProcess.IS_TIMEOUT, result.isTimeout());
metadata.set(ExternalProcess.EXIT_VALUE, result.getExitValue());
@@ -138,10 +158,10 @@ public class ExternalParser implements Parser {
metadata.set(ExternalProcess.STD_ERR_IS_TRUNCATED,
result.isStderrTruncated());
- if (returnStdout) {
+ if (config.isReturnStdout()) {
metadata.set(ExternalProcess.STD_OUT, result.getStdout());
}
- if (returnStderr) {
+ if (config.isReturnStderr()) {
metadata.set(ExternalProcess.STD_ERR, result.getStderr());
}
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata);
@@ -190,95 +210,16 @@ public class ExternalParser implements Parser {
}
/**
- * This is set during initialization from a tika-config.
- * Any calls after initialization will result in a {@link IllegalStateException}.
- *
- * @param supportedTypes
- */
- public void setSupportedTypes(List<String> supportedTypes) {
- if (this.supportedTypes.size() > 0) {
- throw new IllegalStateException("can't set supportedTypes after initialization");
- }
- for (String s : supportedTypes) {
- this.supportedTypes.add(MediaType.parse(s));
- }
- }
-
- public void setTimeoutMs(long timeoutMs) {
- this.timeoutMs = timeoutMs;
- }
-
- public void setMaxStdErr(int maxStdErr) {
- this.maxStdErr = maxStdErr;
- }
-
- public void setMaxStdOut(int maxStdOut) {
- this.maxStdOut = maxStdOut;
- }
-
- /**
- * Use this to specify the full commandLine. The commandline must
- * include at least {@link ExternalParser#INPUT_FILE_TOKEN}.
- * If the external process writes to an output file, specify
- * {@link ExternalParser#OUTPUT_FILE_TOKEN}.
- *
- * @param commandLine
- */
- public void setCommandLine(List<String> commandLine) {
- this.commandLine = commandLine;
- }
-
-
- /**
- * If set to true, this will return the stdout in the metadata
- * via {@link org.apache.tika.metadata.ExternalProcess#STD_OUT}.
- * Default is <code>false</code> because this should normally
- * be handled by the outputParser
- *
- * @param returnStdout
+ * Returns the output parser used to parse the external process output.
*/
- public void setReturnStdout(boolean returnStdout) {
- this.returnStdout = returnStdout;
- }
-
- /**
- * If set to true, this will return the stderr in the metadata
- * via {@link org.apache.tika.metadata.ExternalProcess#STD_ERR}.
- * Default is <code>true</code>
- * @param returnStderr
- */
- public void setReturnStderr(boolean returnStderr) {
- this.returnStderr = returnStderr;
- }
-
- /**
- * This parser is called on the output of the process.
- * If the process writes to an output file, specified by
- * {@link ExternalParser#OUTPUT_FILE_TOKEN}, this parser will parse that file,
- * otherwise it will parse the UTF-8 encoded bytes from the process' STD_OUT.
- * @param parser
- */
- public void setOutputParser(Parser parser) {
- this.outputParser = parser;
- }
-
public Parser getOutputParser() {
return outputParser;
}
- public void checkInitialization()
- throws TikaConfigException {
- if (supportedTypes.size() == 0) {
- throw new TikaConfigException("supportedTypes size must be > 0");
- }
- if (commandLine.isEmpty()) {
- throw new TikaConfigException("commandLine is empty?!");
- }
-
- if (outputParser == EmptyParser.INSTANCE) {
- LOG.debug("no parser selected for the output; contents will be " +
- "written to the content handler");
- }
+ /**
+ * Returns the configuration for this parser.
+ */
+ public ExternalParserConfig getConfig() {
+ return config;
}
-
}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/external2/ExternalParserConfig.java b/tika-core/src/main/java/org/apache/tika/parser/external2/ExternalParserConfig.java
new file mode 100644
index 000000000..913565af8
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/parser/external2/ExternalParserConfig.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.external2;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.tika.parser.Parser;
+
+/**
+ * Configuration for {@link ExternalParser}.
+ * <p>
+ * This config is immutable after construction. ExternalParser does NOT
+ * support runtime configuration changes via ParseContext for security reasons.
+ */
+public class ExternalParserConfig implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private List<String> supportedTypes = new ArrayList<>();
+ private List<String> commandLine = new ArrayList<>();
+ private Parser outputParser;
+ private boolean returnStdout = false;
+ private boolean returnStderr = true;
+ private long timeoutMs = ExternalParser.DEFAULT_TIMEOUT_MS;
+ private int maxStdErr = 10000;
+ private int maxStdOut = 10000;
+
+ public ExternalParserConfig() {
+ }
+
+ public List<String> getSupportedTypes() {
+ return supportedTypes;
+ }
+
+ public void setSupportedTypes(List<String> supportedTypes) {
+ this.supportedTypes = supportedTypes;
+ }
+
+ public List<String> getCommandLine() {
+ return commandLine;
+ }
+
+ public void setCommandLine(List<String> commandLine) {
+ this.commandLine = commandLine;
+ }
+
+ public Parser getOutputParser() {
+ return outputParser;
+ }
+
+ public void setOutputParser(Parser outputParser) {
+ this.outputParser = outputParser;
+ }
+
+ public boolean isReturnStdout() {
+ return returnStdout;
+ }
+
+ public void setReturnStdout(boolean returnStdout) {
+ this.returnStdout = returnStdout;
+ }
+
+ public boolean isReturnStderr() {
+ return returnStderr;
+ }
+
+ public void setReturnStderr(boolean returnStderr) {
+ this.returnStderr = returnStderr;
+ }
+
+ public long getTimeoutMs() {
+ return timeoutMs;
+ }
+
+ public void setTimeoutMs(long timeoutMs) {
+ this.timeoutMs = timeoutMs;
+ }
+
+ public int getMaxStdErr() {
+ return maxStdErr;
+ }
+
+ public void setMaxStdErr(int maxStdErr) {
+ this.maxStdErr = maxStdErr;
+ }
+
+ public int getMaxStdOut() {
+ return maxStdOut;
+ }
+
+ public void setMaxStdOut(int maxStdOut) {
+ this.maxStdOut = maxStdOut;
+ }
+}
diff --git a/tika-core/src/test/java/org/apache/tika/config/ParseContextConfigTest.java b/tika-core/src/test/java/org/apache/tika/config/ParseContextConfigTest.java
index 2ee968765..ad78af6d8 100644
--- a/tika-core/src/test/java/org/apache/tika/config/ParseContextConfigTest.java
+++ b/tika-core/src/test/java/org/apache/tika/config/ParseContextConfigTest.java
@@ -46,7 +46,7 @@ public class ParseContextConfigTest {
}
@Test
- public void testNoConfigContainer() throws Exception {
+ public void testNoJsonConfig() throws Exception {
ParseContext context = new ParseContext();
TestConfig defaultConfig = new TestConfig();
defaultConfig.setName("my-default");
@@ -68,11 +68,9 @@ public class ParseContextConfigTest {
}
@Test
- public void testConfigContainerWithoutMatchingKey() throws Exception {
+ public void testJsonConfigWithoutMatchingKey() throws Exception {
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("other-parser", "{\"name\":\"other\"}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("other-parser", "{\"name\":\"other\"}");
TestConfig defaultConfig = new TestConfig();
defaultConfig.setName("my-default");
@@ -87,9 +85,7 @@ public class ParseContextConfigTest {
@Test
public void testHasConfigTrue() {
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-parser", "{\"name\":\"test\"}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("test-parser", "{\"name\":\"test\"}");
assertTrue(ParseContextConfig.hasConfig(context, "test-parser"));
}
@@ -97,15 +93,13 @@ public class ParseContextConfigTest {
@Test
public void testHasConfigFalse() {
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("other-parser", "{\"name\":\"test\"}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("other-parser", "{\"name\":\"test\"}");
assertFalse(ParseContextConfig.hasConfig(context, "test-parser"));
}
@Test
- public void testHasConfigNoContainer() {
+ public void testHasConfigNoJsonConfigs() {
ParseContext context = new ParseContext();
assertFalse(ParseContextConfig.hasConfig(context, "test-parser"));
diff --git a/tika-core/src/test/java/org/apache/tika/parser/RegexCaptureParserTest.java b/tika-core/src/test/java/org/apache/tika/parser/RegexCaptureParserTest.java
index 421d3987e..bdda6d8de 100644
--- a/tika-core/src/test/java/org/apache/tika/parser/RegexCaptureParserTest.java
+++ b/tika-core/src/test/java/org/apache/tika/parser/RegexCaptureParserTest.java
@@ -39,10 +39,13 @@ public class RegexCaptureParserTest {
"Title: the quick brown fox\n" +
"Author: jumped over\n" +
"Created: 10/20/2024";
- RegexCaptureParser parser = new RegexCaptureParser();
+
Map<String, String> regexes = new HashMap<>();
regexes.put("title", "^Title: ([^\r\n]+)");
- parser.setCaptureMap(regexes);
+
+ RegexCaptureParserConfig config = new RegexCaptureParserConfig();
+ config.setCaptureMap(regexes);
+ RegexCaptureParser parser = new RegexCaptureParser(config);
try (TikaInputStream tis =
TikaInputStream.get(output.getBytes(StandardCharsets.UTF_8))) {
diff --git a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
index 29632929f..577418da9 100644
--- a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
+++ b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java
@@ -49,7 +49,6 @@ import org.apache.tika.ListFetchersRequest;
import org.apache.tika.SaveFetcherReply;
import org.apache.tika.SaveFetcherRequest;
import org.apache.tika.TikaGrpc;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
@@ -160,9 +159,7 @@ class TikaGrpcServerImpl extends TikaGrpc.TikaImplBase {
ParseContext parseContext = new ParseContext();
String additionalFetchConfigJson = request.getAdditionalFetchConfigJson();
if (StringUtils.isNotBlank(additionalFetchConfigJson)) {
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set(request.getFetcherId(), request.getAdditionalFetchConfigJson());
- parseContext.set(ConfigContainer.class, configContainer);
+ parseContext.setJsonConfig(request.getFetcherId(), additionalFetchConfigJson);
}
PipesResult pipesResult = pipesClient.process(new FetchEmitTuple(request.getFetchKey(), new FetchKey(fetcher.getExtensionConfig().id(), request.getFetchKey()),
new EmitKey(), tikaMetadata, parseContext, FetchEmitTuple.ON_PARSE_EXCEPTION.SKIP));
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java
index 904ff38bd..ecc5c8962 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-ocr-module/src/main/java/org/apache/tika/parser/ocr/TesseractOCRParser.java
@@ -58,7 +58,6 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.ConfigDeserializer;
import org.apache.tika.config.Initializable;
import org.apache.tika.config.JsonConfig;
@@ -299,10 +298,8 @@ public class TesseractOCRParser extends AbstractExternalProcessParser implements
}
private TesseractOCRConfig getConfig(ParseContext parseContext) throws TikaConfigException, IOException {
- // Check for ConfigContainer with component-specific runtime config
- ConfigContainer configContainer =
- parseContext.get(org.apache.tika.config.ConfigContainer.class);
- if (configContainer != null && configContainer.get("tesseract-ocr-parser").isPresent()) {
+ // Check for JSON config with component-specific runtime config
+ if (parseContext.hasJsonConfig("tesseract-ocr-parser")) {
// First validate that no paths are being set by deserializing into RuntimeConfig
// The RuntimeConfig setters will throw TikaConfigException if JSON contains path fields
TesseractOCRConfig.RuntimeConfig runtimeConfig = ParseContextConfig.getConfig(
@@ -553,7 +550,7 @@ public class TesseractOCRParser extends AbstractExternalProcessParser implements
* Reads the contents of the given stream and write it to the given XHTML
* content handler. The stream is closed once fully processed.
*
- * @param stream Stream where is the result of ocr
+ * @param tis Stream where is the result of ocr
* @param xhtml XHTML content handler
* @throws SAXException if the XHTML SAX events could not be handled
* @throws IOException if an input error occurred
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
index f2f61a264..d634dfa92 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
@@ -254,7 +254,7 @@ public class PDFParser implements Parser, RenderingParser {
private PDFParserConfig getConfig(ParseContext parseContext) throws TikaException, IOException {
// ParseContextConfig.getConfig() handles:
// 1. Check for PDFParserConfig already in ParseContext (fast path for embedded docs)
- // 2. Check ConfigContainer for "pdf-parser" and deserialize if present
+ // 2. Check jsonConfigs for "pdf-parser" and deserialize if present
// 3. Set deserialized config in ParseContext for future lookups
// 4. Return defaultConfig if no runtime config found
return ParseContextConfig.getConfig(
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java
index 4f14f0cde..c80045e36 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/ocr/TesseractOCRParserTest.java
@@ -33,7 +33,6 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.apache.tika.TikaTest;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.ParseContextConfig;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.config.loader.TikaObjectMapperFactory;
@@ -300,11 +299,9 @@ public class TesseractOCRParserTest extends TikaTest {
otherConfig.put("k2", "b2");
runtimeUpdates.put("otherTesseractConfig", otherConfig);
- // Store runtime config in ConfigContainer
+ // Store runtime config in ParseContext
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("tesseract-ocr-parser", mapper.writeValueAsString(runtimeUpdates));
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("tesseract-ocr-parser", mapper.writeValueAsString(runtimeUpdates));
// Merge configs using ParseContextConfig
TesseractOCRConfig mergedConfig = ParseContextConfig.getConfig(
@@ -334,9 +331,7 @@ public class TesseractOCRParserTest extends TikaTest {
configWithTesseractPath.put("tesseractPath", "/some/path");
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("tesseract-ocr-parser", mapper.writeValueAsString(configWithTesseractPath));
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("tesseract-ocr-parser", mapper.writeValueAsString(configWithTesseractPath));
IOException exception = assertThrows(IOException.class, () -> {
ParseContextConfig.getConfig(
@@ -351,9 +346,7 @@ public class TesseractOCRParserTest extends TikaTest {
Map<String, Object> configWithTessdataPath = new HashMap<>();
configWithTessdataPath.put("tessdataPath", "/some/path");
- configContainer = new ConfigContainer();
- configContainer.set("tesseract-ocr-parser", mapper.writeValueAsString(configWithTessdataPath));
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("tesseract-ocr-parser", mapper.writeValueAsString(configWithTessdataPath));
exception = assertThrows(IOException.class, () -> {
ParseContextConfig.getConfig(
@@ -368,9 +361,7 @@ public class TesseractOCRParserTest extends TikaTest {
Map<String, Object> configWithImageMagickPath = new HashMap<>();
configWithImageMagickPath.put("imageMagickPath", "/some/path");
- configContainer = new ConfigContainer();
- configContainer.set("tesseract-ocr-parser", mapper.writeValueAsString(configWithImageMagickPath));
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("tesseract-ocr-parser", mapper.writeValueAsString(configWithImageMagickPath));
exception = assertThrows(IOException.class, () -> {
ParseContextConfig.getConfig(
@@ -386,9 +377,7 @@ public class TesseractOCRParserTest extends TikaTest {
validRuntimeConfig.put("language", "fra");
validRuntimeConfig.put("skipOcr", true);
- configContainer = new ConfigContainer();
- configContainer.set("tesseract-ocr-parser", mapper.writeValueAsString(validRuntimeConfig));
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("tesseract-ocr-parser", mapper.writeValueAsString(validRuntimeConfig));
// This should not throw
TesseractOCRConfig.RuntimeConfig runtimeConfig = ParseContextConfig.getConfig(
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
index a0b69ee48..cda95cc56 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
@@ -68,6 +68,8 @@ import org.apache.tika.parser.ocr.TesseractOCRParser;
import org.apache.tika.parser.xml.XMLProfiler;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.RecursiveParserWrapperHandler;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+import org.apache.tika.serialization.serdes.ParseContextSerializer;
import org.apache.tika.utils.StringUtils;
public class PDFParserTest extends TikaTest {
@@ -591,8 +593,8 @@ public class PDFParserTest extends TikaTest {
// Serialize using ParseContextSerializer
com.fasterxml.jackson.databind.ObjectMapper mapper = TikaObjectMapperFactory.getMapper();
com.fasterxml.jackson.databind.module.SimpleModule module = new com.fasterxml.jackson.databind.module.SimpleModule();
- module.addSerializer(ParseContext.class, new org.apache.tika.serialization.ParseContextSerializer());
- module.addDeserializer(ParseContext.class, new org.apache.tika.serialization.ParseContextDeserializer());
+ module.addSerializer(ParseContext.class, new ParseContextSerializer());
+ module.addDeserializer(ParseContext.class, new ParseContextDeserializer());
mapper.registerModule(module);
String json = mapper.writeValueAsString(parseContext);
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java
index 767e4ef3c..be76398d7 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleDeserializer.java
@@ -26,7 +26,7 @@ import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.
import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.ID;
import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.METADATA_KEY;
import static org.apache.tika.pipes.core.serialization.FetchEmitTupleSerializer.ON_PARSE_EXCEPTION;
-import static org.apache.tika.serialization.ParseContextSerializer.PARSE_CONTEXT;
+import static org.apache.tika.serialization.serdes.ParseContextSerializer.PARSE_CONTEXT;
import java.io.IOException;
import java.util.Map;
@@ -42,8 +42,8 @@ import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.FetchEmitTuple;
import org.apache.tika.pipes.api.emitter.EmitKey;
import org.apache.tika.pipes.api.fetcher.FetchKey;
-import org.apache.tika.serialization.ParseContextDeserializer;
import org.apache.tika.serialization.ParseContextUtils;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
public class FetchEmitTupleDeserializer extends JsonDeserializer<FetchEmitTuple> {
@@ -61,7 +61,7 @@ public class FetchEmitTupleDeserializer extends JsonDeserializer<FetchEmitTuple>
Metadata metadata = readMetadata(root);
JsonNode parseContextNode = root.get(PARSE_CONTEXT);
ParseContext parseContext = parseContextNode == null ? new ParseContext() : ParseContextDeserializer.readParseContext(parseContextNode);
- // Resolve all friendly-named components from ConfigContainer to actual objects
+ // Resolve all friendly-named components from jsonConfigs to actual objects
ParseContextUtils.resolveAll(parseContext, FetchEmitTupleDeserializer.class.getClassLoader());
FetchEmitTuple.ON_PARSE_EXCEPTION onParseException = readOnParseException(root);
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleSerializer.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleSerializer.java
index 9a236b1d3..9dc06b560 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleSerializer.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/FetchEmitTupleSerializer.java
@@ -16,7 +16,7 @@
*/
package org.apache.tika.pipes.core.serialization;
-import static org.apache.tika.serialization.ParseContextSerializer.PARSE_CONTEXT;
+import static org.apache.tika.serialization.serdes.ParseContextSerializer.PARSE_CONTEXT;
import java.io.IOException;
import java.util.Locale;
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonEmitData.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonEmitData.java
index 17b2e3238..840991db8 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonEmitData.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonEmitData.java
@@ -26,8 +26,8 @@ import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.FetchEmitTuple;
import org.apache.tika.pipes.core.emitter.EmitDataImpl;
-import org.apache.tika.serialization.MetadataSerializer;
-import org.apache.tika.serialization.ParseContextSerializer;
+import org.apache.tika.serialization.serdes.MetadataSerializer;
+import org.apache.tika.serialization.serdes.ParseContextSerializer;
public class JsonEmitData {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTuple.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTuple.java
index 72ae9c225..cdf89be42 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTuple.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTuple.java
@@ -27,9 +27,9 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.FetchEmitTuple;
-import org.apache.tika.serialization.MetadataSerializer;
-import org.apache.tika.serialization.ParseContextDeserializer;
-import org.apache.tika.serialization.ParseContextSerializer;
+import org.apache.tika.serialization.serdes.MetadataSerializer;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+import org.apache.tika.serialization.serdes.ParseContextSerializer;
public class JsonFetchEmitTuple {
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java
index 9c6c6369e..64f8f8eab 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/serialization/JsonFetchEmitTupleList.java
@@ -29,9 +29,9 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.FetchEmitTuple;
-import org.apache.tika.serialization.MetadataSerializer;
-import org.apache.tika.serialization.ParseContextDeserializer;
-import org.apache.tika.serialization.ParseContextSerializer;
+import org.apache.tika.serialization.serdes.MetadataSerializer;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+import org.apache.tika.serialization.serdes.ParseContextSerializer;
public class JsonFetchEmitTupleList {
diff --git a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PipesClientTest.java b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PipesClientTest.java
index 9fcd2dd06..b85bcdf51 100644
--- a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PipesClientTest.java
+++ b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/PipesClientTest.java
@@ -28,7 +28,6 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.TikaTaskTimeout;
import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.metadata.Metadata;
@@ -108,24 +107,22 @@ public class PipesClientTest {
@Test
public void testMetadataFilterFromJsonConfig(@TempDir Path tmp) throws Exception {
- // Test that metadata filters specified as JSON array in ConfigContainer
+ // Test that metadata filters specified as JSON array in jsonConfigs
// are properly resolved and applied during pipe processing.
// This tests the full serialization/deserialization flow.
ParseContext parseContext = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("metadata-filters", """
+ parseContext.setJsonConfig("metadata-filters", """
[
"mock-upper-case-filter"
]
""");
- parseContext.set(ConfigContainer.class, configContainer);
// Resolve the config to actual MetadataFilter instances
ParseContextUtils.resolveAll(parseContext, PipesClientTest.class.getClassLoader());
// Verify the filter was resolved
MetadataFilter resolvedFilter = parseContext.get(MetadataFilter.class);
- Assertions.assertNotNull(resolvedFilter, "MetadataFilter should be resolved from ConfigContainer");
+ Assertions.assertNotNull(resolvedFilter, "MetadataFilter should be resolved from jsonConfigs");
assertEquals(CompositeMetadataFilter.class, resolvedFilter.getClass());
PipesClient pipesClient = init(tmp, testDoc);
@@ -144,14 +141,12 @@ public class PipesClientTest {
public void testMultipleMetadataFiltersFromJsonConfig(@TempDir Path tmp) throws Exception {
// Test multiple filters specified as JSON array
ParseContext parseContext = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("metadata-filters", """
+ parseContext.setJsonConfig("metadata-filters", """
[
"attachment-counting-list-filter",
"mock-upper-case-filter"
]
""");
- parseContext.set(ConfigContainer.class, configContainer);
// Resolve the config to actual MetadataFilter instances
ParseContextUtils.resolveAll(parseContext, PipesClientTest.class.getClassLoader());
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
index 3e785c640..a3cfee131 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
@@ -27,12 +27,10 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.List;
-import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.JsonConfig;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.metadata.Metadata;
@@ -173,27 +171,23 @@ public class FileSystemEmitter extends AbstractStreamEmitter {
private FileSystemEmitterConfig getConfig(ParseContext parseContext) throws TikaConfigException, IOException {
FileSystemEmitterConfig config = fileSystemEmitterConfig;
- ConfigContainer configContainer = parseContext.get(ConfigContainer.class);
- if (configContainer != null) {
- Optional<JsonConfig> configJson = configContainer.get(getExtensionConfig().id());
- if (configJson.isPresent()) {
+ String configKey = getExtensionConfig().id();
+ if (parseContext.hasJsonConfig(configKey)) {
+ JsonConfig configJson = parseContext.getJsonConfig(configKey);
+ if (configJson != null) {
// Check if basePath is present in runtime config - this is not allowed for security
- if (configJson
- .get()
- .json()
- .contains("\"basePath\"")) {
+ if (configJson.json().contains("\"basePath\"")) {
throw new TikaConfigException("Cannot change 'basePath' at runtime for security reasons. " + "basePath can only be set during initialization.");
}
// Load runtime config (excludes basePath for security)
- FileSystemEmitterRuntimeConfig runtimeConfig = FileSystemEmitterRuntimeConfig.load(configJson.get().json());
+ FileSystemEmitterRuntimeConfig runtimeConfig = FileSystemEmitterRuntimeConfig.load(configJson.json());
// Merge runtime config into default config while preserving basePath
config = new FileSystemEmitterConfig(fileSystemEmitterConfig.basePath(), runtimeConfig.getFileExtension(), runtimeConfig.getOnExists(),
runtimeConfig.isPrettyPrint());
checkConfig(config);
}
-
}
return config;
}
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java
index b3f58d54c..c13bef300 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterRuntimeConfigTest.java
@@ -34,14 +34,13 @@ import java.util.Locale;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.plugins.ExtensionConfig;
/**
- * Tests runtime configuration of FileSystemEmitter via ConfigContainer and ParseContext.
+ * Tests runtime configuration of FileSystemEmitter via ParseContext's jsonConfigs.
*/
public class FileSystemEmitterRuntimeConfigTest {
@@ -77,11 +76,9 @@ public class FileSystemEmitterRuntimeConfigTest {
String runtimeConfig = String.format(Locale.ROOT,
"{\"basePath\":\"%s\", \"onExists\":\"REPLACE\"}",
dir2.toString().replace("\\", "\\\\"));
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-emitter", runtimeConfig);
ParseContext context2 = new ParseContext();
- context2.set(ConfigContainer.class, configContainer);
+ context2.setJsonConfig("test-emitter", runtimeConfig);
// Emit with runtime config - should throw exception
List<Metadata> metadataList2 = new ArrayList<>();
@@ -121,11 +118,9 @@ public class FileSystemEmitterRuntimeConfigTest {
// Override at runtime to add .json extension
// Note: basePath is NOT included for security reasons
String runtimeConfig = "{\"fileExtension\":\"json\", \"onExists\":\"REPLACE\"}";
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-emitter", runtimeConfig);
ParseContext context2 = new ParseContext();
- context2.set(ConfigContainer.class, configContainer);
+ context2.setJsonConfig("test-emitter", runtimeConfig);
// Emit with runtime config
List<Metadata> metadataList2 = new ArrayList<>();
@@ -164,11 +159,9 @@ public class FileSystemEmitterRuntimeConfigTest {
// Override at runtime to use SKIP
// Note: basePath is NOT included for security reasons
String runtimeConfig = "{\"onExists\":\"SKIP\"}";
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-emitter", runtimeConfig);
ParseContext context2 = new ParseContext();
- context2.set(ConfigContainer.class, configContainer);
+ context2.setJsonConfig("test-emitter", runtimeConfig);
// Emit with runtime config (SKIP) - should not replace existing file
InputStream inputStream2 = new ByteArrayInputStream("new content".getBytes(StandardCharsets.UTF_8));
@@ -180,7 +173,7 @@ public class FileSystemEmitterRuntimeConfigTest {
}
@Test
- public void testConfigContainerNotPresent(@TempDir Path tempDir) throws Exception {
+ public void testJsonConfigNotPresent(@TempDir Path tempDir) throws Exception {
// Create emitter with default config
String defaultConfig = String.format(Locale.ROOT,
"{\"basePath\":\"%s\", \"onExists\":\"REPLACE\"}",
@@ -188,14 +181,14 @@ public class FileSystemEmitterRuntimeConfigTest {
ExtensionConfig pluginConfig = new ExtensionConfig("test-emitter", "test", defaultConfig);
FileSystemEmitter emitter = FileSystemEmitter.build(pluginConfig);
- // Emit with ParseContext that has no ConfigContainer - should use default config
+ // Emit with ParseContext that has no jsonConfigs - should use default config
List<Metadata> metadataList = new ArrayList<>();
Metadata m = new Metadata();
m.set(TikaCoreProperties.TIKA_CONTENT, "test content");
metadataList.add(m);
ParseContext context = new ParseContext();
- // Don't set ConfigContainer in context
+ // Don't set jsonConfigs in context
emitter.emit("test.json", metadataList, context);
@@ -204,7 +197,7 @@ public class FileSystemEmitterRuntimeConfigTest {
}
@Test
- public void testConfigContainerWithDifferentId(@TempDir Path tempDir) throws Exception {
+ public void testJsonConfigWithDifferentId(@TempDir Path tempDir) throws Exception {
// Create emitter with default config
String defaultConfig = String.format(Locale.ROOT,
"{\"basePath\":\"%s\", \"onExists\":\"REPLACE\"}",
@@ -212,18 +205,16 @@ public class FileSystemEmitterRuntimeConfigTest {
ExtensionConfig pluginConfig = new ExtensionConfig("test-emitter", "test", defaultConfig);
FileSystemEmitter emitter = FileSystemEmitter.build(pluginConfig);
- // Create ConfigContainer with config for a different emitter ID
+ // Create jsonConfigs with config for a different emitter ID
Path otherDir = tempDir.resolve("other");
Files.createDirectories(otherDir);
- ConfigContainer configContainer = new ConfigContainer();
String runtimeConfig = String.format(Locale.ROOT,
"{\"basePath\":\"%s\", \"onExists\":\"REPLACE\"}",
otherDir.toString().replace("\\", "\\\\"));
- configContainer.set("different-emitter", runtimeConfig);
ParseContext context = new ParseContext();
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("different-emitter", runtimeConfig);
// Emit - should use default config since runtime config is for different ID
List<Metadata> metadataList = new ArrayList<>();
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
index 748614d5c..3ab495734 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
@@ -34,7 +34,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
@@ -62,7 +61,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.tika.client.HttpClientFactory;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.JsonConfig;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
@@ -205,16 +203,14 @@ public class HttpFetcher extends AbstractTikaExtension implements Fetcher, Range
}
private HttpFetcherConfig getAdditionalHttpFetcherConfig(ParseContext parseContext) throws JsonProcessingException {
- HttpFetcherConfig additionalHttpFetcherConfig = null;
- ConfigContainer configContainer = parseContext.get(ConfigContainer.class);
- if (configContainer == null) {
- return null;
- }
- Optional<JsonConfig> jsonOpt = configContainer.get(HttpFetcher.class);
- if (jsonOpt.isPresent()) {
- additionalHttpFetcherConfig = OM.readValue(jsonOpt.get().json(), HttpFetcherConfig.class);
+ // Check for JSON config in ParseContext (new pattern)
+ if (parseContext.hasJsonConfig("http-fetcher")) {
+ JsonConfig jsonConfig = parseContext.getJsonConfig("http-fetcher");
+ if (jsonConfig != null) {
+ return OM.readValue(jsonConfig.json(), HttpFetcherConfig.class);
+ }
}
- return additionalHttpFetcherConfig;
+ return null;
}
@Override
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
index f20c2c392..95c33cfc7 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
@@ -60,7 +60,6 @@ import org.mockito.Mockito;
import org.apache.tika.TikaTest;
import org.apache.tika.client.HttpClientFactory;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TemporaryResources;
@@ -206,9 +205,7 @@ class HttpFetcherTest extends TikaTest {
headersMap.put("fromFetchRequestHeader1", List.of("fromFetchRequestValue1"));
headersMap.put("fromFetchRequestHeader2", List.of("fromFetchRequestValue2", "fromFetchRequestValue3"));
additionalHttpFetcherConfig.getHttpRequestHeaders().setMap(headersMap);
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set(HttpFetcher.class, new ObjectMapper().writeValueAsString(additionalHttpFetcherConfig));
- parseContext.set(ConfigContainer.class, configContainer);
+ parseContext.setJsonConfig("http-fetcher", new ObjectMapper().writeValueAsString(additionalHttpFetcherConfig));
httpFetcher.getHttpFetcherConfig().setHttpRequestHeaders(new HttpHeaders());
HashMap<String, Collection<String>> headersMapFromConfig = new HashMap<>();
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInfo.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInfo.java
index 72c7156b2..fa96517aa 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInfo.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInfo.java
@@ -21,7 +21,7 @@ package org.apache.tika.config.loader;
*
* @param componentClass the component's class
* @param selfConfiguring whether the component implements SelfConfiguring
- * (reads its own config from ConfigContainer)
+ * (reads its own config from ParseContext's jsonConfigs)
* @param contextKey the class to use as the key when adding to ParseContext,
* or null to auto-detect based on known interfaces
*/
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/CompositeComponentLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/CompositeComponentLoader.java
deleted file mode 100644
index 6b65658ab..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/CompositeComponentLoader.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.config.loader;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.ServiceLoader;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.tika.exception.TikaConfigException;
-
-/**
- * Generic loader for Tika components (detectors, encoding detectors, filters, etc.).
- * Supports two loading modes:
- * <ul>
- * <li>Array-based: explicit ordering, no SPI fallback (default for non-parsers)</li>
- * <li>SPI-only: all components from ServiceLoader (when config section missing)</li>
- * </ul>
- *
- * @param <T> the component type
- */
-public class CompositeComponentLoader<T> {
-
- private static final Logger LOG = LoggerFactory.getLogger(CompositeComponentLoader.class);
-
- private final Class<T> componentInterface;
- private final String componentTypeName;
- private final ClassLoader classLoader;
- private final ObjectMapper objectMapper;
-
- /**
- * Creates a component loader.
- *
- * @param componentInterface the component interface (e.g., Detector.class)
- * @param componentTypeName the JSON config key (e.g., "detectors")
- * @param classLoader the class loader
- * @param objectMapper the Jackson ObjectMapper
- */
- public CompositeComponentLoader(Class<T> componentInterface, String componentTypeName,
- ClassLoader classLoader, ObjectMapper objectMapper) {
- this.componentInterface = componentInterface;
- this.componentTypeName = componentTypeName;
- this.classLoader = classLoader;
- this.objectMapper = objectMapper;
- }
-
- /**
- * Loads components from array-based JSON config.
- * If config section exists, uses only explicitly listed components (no SPI fallback).
- * If config section missing, uses SPI to discover all components.
- *
- * @param config the Tika JSON configuration
- * @return ordered list of component instances
- * @throws TikaConfigException if loading fails
- */
- public List<T> loadFromArray(TikaJsonConfig config) throws TikaConfigException {
- // Check if section exists in config
- if (!config.hasComponentSection(componentTypeName)) {
- // Section doesn't exist - use SPI fallback
- return loadAllFromSpi();
- }
-
- // Check if section is using object format instead of array format
- JsonNode sectionNode = config.getRootNode().get(componentTypeName);
- if (sectionNode != null && !sectionNode.isArray()) {
- throw new TikaConfigException(
- "Configuration section '" + componentTypeName + "' must be an array, not an object. " +
- "Expected format: \"" + componentTypeName + "\": [{\"component-name\": {...}}], " +
- "Got object format: \"" + componentTypeName + "\": {\"component-name\": {...}}");
- }
-
- // Section exists - load only explicitly configured components (no SPI)
- List<Map.Entry<String, JsonNode>> arrayComponents = config.getArrayComponents(componentTypeName);
-
- if (arrayComponents.isEmpty()) {
- // Explicit empty array means no components
- return Collections.emptyList();
- }
-
- List<T> instances = new ArrayList<>();
-
- for (Map.Entry<String, JsonNode> entry : arrayComponents) {
- String name = entry.getKey();
- JsonNode configNode = entry.getValue();
-
- T instance = deserializeComponent(name, configNode);
- instances.add(instance);
- }
-
- return instances;
- }
-
- /**
- * Loads components from JSON config with SPI fallback (used by parsers).
- *
- * @param config the Tika JSON configuration
- * @return list of component instances
- * @throws TikaConfigException if loading fails
- */
- public List<T> load(TikaJsonConfig config) throws TikaConfigException {
- List<T> instances = new ArrayList<>();
-
- // Load configured components
- if (config.hasComponents(componentTypeName)) {
- Map<String, JsonNode> components = config.getComponents(componentTypeName);
-
- for (Map.Entry<String, JsonNode> entry : components.entrySet()) {
- String name = entry.getKey();
- JsonNode configNode = entry.getValue();
-
- T instance = deserializeComponent(name, configNode);
- instances.add(instance);
- }
- }
-
- // Add SPI-discovered components
- List<T> spiComponents = loadAllFromSpi();
- instances.addAll(spiComponents);
-
- return instances;
- }
-
- /**
- * Deserializes a component, trying JsonConfig constructor first, then Jackson bean deserialization.
- */
- private T deserializeComponent(String name, JsonNode configNode) throws TikaConfigException {
- return ComponentInstantiator.instantiate(name, configNode, objectMapper, classLoader);
- }
-
- private List<T> loadAllFromSpi() {
- List<T> result = new ArrayList<>();
- ServiceLoader<T> serviceLoader = ServiceLoader.load(componentInterface, classLoader);
-
- Iterator<T> iterator = serviceLoader.iterator();
- while (iterator.hasNext()) {
- try {
- T instance = iterator.next();
- result.add(instance);
- } catch (Exception e) {
- // Log and skip problematic SPI providers
- LOG.warn("Failed to load SPI component of type {}: {}", componentTypeName, e.getMessage(), e);
- }
- }
-
- return result;
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/DetectorLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/DetectorLoader.java
deleted file mode 100644
index a277a9b79..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/DetectorLoader.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.config.loader;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.tika.detect.CompositeDetector;
-import org.apache.tika.detect.DefaultDetector;
-import org.apache.tika.detect.Detector;
-import org.apache.tika.exception.TikaConfigException;
-
-/**
- * Loader for detectors with support for SPI fallback via "default-detector" marker.
- */
-public class DetectorLoader {
-
- private static final Logger LOG = LoggerFactory.getLogger(DetectorLoader.class);
-
- private final ClassLoader classLoader;
- private final ObjectMapper objectMapper;
-
- public DetectorLoader(ClassLoader classLoader, ObjectMapper objectMapper) {
- this.classLoader = classLoader;
- this.objectMapper = objectMapper;
- }
-
- /**
- * Loads detectors from JSON config and builds a CompositeDetector.
- * <p>
- * Supports "default-detector" marker for SPI fallback with optional exclusions:
- * <ul>
- * <li>If "detectors" section exists:
- * <ul>
- * <li>If "default-detector" is present: loads configured detectors + SPI detectors (minus exclusions)</li>
- * <li>If "default-detector" is absent: loads only configured detectors (no SPI)</li>
- * </ul>
- * </li>
- * <li>If "detectors" section missing: uses DefaultDetector to discover all detectors via SPI</li>
- * </ul>
- *
- * @param config the Tika JSON configuration
- * @return the composite detector
- * @throws TikaConfigException if loading fails
- */
- public Detector load(TikaJsonConfig config) throws TikaConfigException {
- // Load configured detectors
- if (config.hasComponentSection("detectors")) {
- List<Detector> detectorList = new ArrayList<>();
- List<Map.Entry<String, JsonNode>> detectors = config.getArrayComponents("detectors");
-
- // Check if "default-detector" is in the list and extract exclusions
- boolean hasDefaultDetector = false;
- Set<Class<? extends Detector>> excludedDetectorClasses = new HashSet<>();
-
- for (Map.Entry<String, JsonNode> entry : detectors) {
- if ("default-detector".equals(entry.getKey())) {
- hasDefaultDetector = true;
- excludedDetectorClasses.addAll(parseExclusions(entry.getValue()));
- break;
- }
- }
-
- // Track configured detector classes to avoid SPI duplicates
- Set<Class<? extends Detector>> configuredDetectorClasses = new HashSet<>();
-
- // Load configured detectors (skip "default-detector" marker)
- for (Map.Entry<String, JsonNode> entry : detectors) {
- String name = entry.getKey();
-
- // Skip the special "default-detector" marker
- if ("default-detector".equals(name)) {
- continue;
- }
-
- // Special case: mime-types requires the initialized registry from TikaLoader
- if ("mime-types".equals(name)) {
- LOG.debug("Using TikaLoader.getMimeTypes() for mime-types detector");
- detectorList.add(TikaLoader.getMimeTypes());
- configuredDetectorClasses.add(TikaLoader.getMimeTypes().getClass());
- continue;
- }
-
- // Use Jackson with mixins to deserialize - the TypeIdResolver handles name resolution
- Detector detector = deserializeDetector(name, entry.getValue());
- detectorList.add(detector);
- @SuppressWarnings("unchecked")
- Class<? extends Detector> detectorClass =
- (Class<? extends Detector>) detector.getClass();
- configuredDetectorClasses.add(detectorClass);
- }
-
- // Add excluded detectors to the configured set so they won't be loaded from SPI
- configuredDetectorClasses.addAll(excludedDetectorClasses);
-
- // Add SPI-discovered detectors only if "default-detector" is in config
- if (hasDefaultDetector) {
- DefaultDetector defaultDetector = createDefaultDetector(configuredDetectorClasses);
- LOG.debug("Loading SPI detectors because 'default-detector' is in config");
- if (detectorList.isEmpty()) {
- return defaultDetector;
- }
- detectorList.add(0, defaultDetector);
- } else {
- LOG.debug("Skipping SPI detectors - 'default-detector' not in config");
- }
-
- return new CompositeDetector(TikaLoader.getMediaTypeRegistry(), detectorList);
- } else {
- // No configured detectors - use DefaultDetector to load all from SPI
- return createDefaultDetector(Collections.emptySet());
- }
- }
-
- /**
- * Deserializes a detector, trying JsonConfig constructor first, then Jackson bean deserialization.
- */
- private Detector deserializeDetector(String name, JsonNode configNode) throws TikaConfigException {
- return ComponentInstantiator.instantiate(name, configNode, objectMapper, classLoader);
- }
-
- /**
- * Parses exclusion list from default-detector config.
- */
- @SuppressWarnings("unchecked")
- private Set<Class<? extends Detector>> parseExclusions(JsonNode configNode) {
- Set<Class<? extends Detector>> excluded = new HashSet<>();
- if (configNode == null || !configNode.has("_exclude")) {
- return excluded;
- }
-
- JsonNode excludeNode = configNode.get("_exclude");
- if (!excludeNode.isArray()) {
- return excluded;
- }
-
- for (JsonNode excludeName : excludeNode) {
- if (!excludeName.isTextual()) {
- continue;
- }
- String detectorName = excludeName.asText();
- try {
- // Try to resolve via TypeIdResolver's logic (registry lookup then Class.forName)
- Class<?> detectorClass = resolveClass(detectorName);
- excluded.add((Class<? extends Detector>) detectorClass);
- LOG.debug("Excluding detector from SPI: {}", detectorName);
- } catch (Exception e) {
- LOG.warn("Unknown detector in exclude list: {}", detectorName);
- }
- }
- return excluded;
- }
-
- /**
- * Resolves a name to a class, trying friendly name lookup first then FQCN.
- */
- private Class<?> resolveClass(String name) throws ClassNotFoundException {
- return org.apache.tika.serialization.ComponentNameResolver
- .resolveClass(name, classLoader);
- }
-
- /**
- * Creates a DefaultDetector that loads detectors from SPI, excluding the specified classes.
- *
- * @param excludeClasses detector classes to exclude from SPI loading
- * @return the DefaultDetector with SPI-loaded detectors
- */
- private DefaultDetector createDefaultDetector(Collection<Class<? extends Detector>> excludeClasses) {
- return new DefaultDetector(TikaLoader.getMimeTypes(),
- new org.apache.tika.config.ServiceLoader(classLoader),
- excludeClasses);
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/EncodingDetectorLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/EncodingDetectorLoader.java
deleted file mode 100644
index 25668d38e..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/EncodingDetectorLoader.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.config.loader;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.tika.detect.CompositeEncodingDetector;
-import org.apache.tika.detect.DefaultEncodingDetector;
-import org.apache.tika.detect.EncodingDetector;
-import org.apache.tika.exception.TikaConfigException;
-
-/**
- * Loader for encoding detectors with support for SPI fallback via "default-encoding-detector" marker.
- */
-public class EncodingDetectorLoader {
-
- private static final Logger LOG = LoggerFactory.getLogger(EncodingDetectorLoader.class);
-
- private final ClassLoader classLoader;
- private final ObjectMapper objectMapper;
-
- public EncodingDetectorLoader(ClassLoader classLoader, ObjectMapper objectMapper) {
- this.classLoader = classLoader;
- this.objectMapper = objectMapper;
- }
-
- /**
- * Loads encoding detectors from JSON config and builds a CompositeEncodingDetector.
- * <p>
- * Supports "default-encoding-detector" marker for SPI fallback with optional exclusions:
- * <ul>
- * <li>If "encoding-detectors" section exists:
- * <ul>
- * <li>If "default-encoding-detector" is present: loads configured detectors + SPI detectors (minus exclusions)</li>
- * <li>If "default-encoding-detector" is absent: loads only configured detectors (no SPI)</li>
- * </ul>
- * </li>
- * <li>If "encoding-detectors" section missing: uses DefaultEncodingDetector to discover all detectors via SPI</li>
- * </ul>
- *
- * @param config the Tika JSON configuration
- * @return the composite encoding detector
- * @throws TikaConfigException if loading fails
- */
- public EncodingDetector load(TikaJsonConfig config) throws TikaConfigException {
- // Load configured encoding detectors
- if (config.hasComponentSection("encoding-detectors")) {
- List<EncodingDetector> detectorList = new ArrayList<>();
- List<Map.Entry<String, JsonNode>> detectors = config.getArrayComponents("encoding-detectors");
-
- // Check if "default-encoding-detector" is in the list and extract exclusions
- boolean hasDefaultEncodingDetector = false;
- Set<Class<? extends EncodingDetector>> excludedDetectorClasses = new HashSet<>();
-
- for (Map.Entry<String, JsonNode> entry : detectors) {
- if ("default-encoding-detector".equals(entry.getKey())) {
- hasDefaultEncodingDetector = true;
- excludedDetectorClasses.addAll(parseExclusions(entry.getValue()));
- break;
- }
- }
-
- // Track configured detector classes to avoid SPI duplicates
- Set<Class<? extends EncodingDetector>> configuredDetectorClasses = new HashSet<>();
-
- // Load configured encoding detectors (skip "default-encoding-detector" marker)
- for (Map.Entry<String, JsonNode> entry : detectors) {
- String name = entry.getKey();
-
- // Skip the special "default-encoding-detector" marker
- if ("default-encoding-detector".equals(name)) {
- continue;
- }
-
- // Use Jackson with mixins to deserialize
- EncodingDetector detector = deserializeEncodingDetector(name, entry.getValue());
- detectorList.add(detector);
- @SuppressWarnings("unchecked")
- Class<? extends EncodingDetector> detectorClass =
- (Class<? extends EncodingDetector>) detector.getClass();
- configuredDetectorClasses.add(detectorClass);
- }
-
- // Add excluded detectors to the configured set so they won't be loaded from SPI
- configuredDetectorClasses.addAll(excludedDetectorClasses);
-
- // Add SPI-discovered detectors only if "default-encoding-detector" is in config
- if (hasDefaultEncodingDetector) {
- DefaultEncodingDetector defaultDetector = createDefaultEncodingDetector(configuredDetectorClasses);
- LOG.debug("Loading SPI encoding detectors because 'default-encoding-detector' is in config");
- if (detectorList.isEmpty()) {
- return defaultDetector;
- }
- detectorList.add(0, defaultDetector);
- } else {
- LOG.debug("Skipping SPI encoding detectors - 'default-encoding-detector' not in config");
- }
-
- return new CompositeEncodingDetector(detectorList);
- } else {
- // No configured encoding detectors - use DefaultEncodingDetector to load all from SPI
- return createDefaultEncodingDetector(Collections.emptySet());
- }
- }
-
- /**
- * Deserializes an encoding detector, trying JsonConfig constructor first, then Jackson bean deserialization.
- */
- private EncodingDetector deserializeEncodingDetector(String name, JsonNode configNode)
- throws TikaConfigException {
- return ComponentInstantiator.instantiate(name, configNode, objectMapper, classLoader);
- }
-
- /**
- * Parses exclusion list from default-encoding-detector config.
- */
- @SuppressWarnings("unchecked")
- private Set<Class<? extends EncodingDetector>> parseExclusions(JsonNode configNode) {
- Set<Class<? extends EncodingDetector>> excluded = new HashSet<>();
- if (configNode == null || !configNode.has("_exclude")) {
- return excluded;
- }
-
- JsonNode excludeNode = configNode.get("_exclude");
- if (!excludeNode.isArray()) {
- return excluded;
- }
-
- for (JsonNode excludeName : excludeNode) {
- if (!excludeName.isTextual()) {
- continue;
- }
- String detectorName = excludeName.asText();
- try {
- Class<?> detectorClass = resolveClass(detectorName);
- excluded.add((Class<? extends EncodingDetector>) detectorClass);
- LOG.debug("Excluding encoding detector from SPI: {}", detectorName);
- } catch (Exception e) {
- LOG.warn("Unknown encoding detector in exclude list: {}", detectorName);
- }
- }
- return excluded;
- }
-
- /**
- * Resolves a name to a class, trying friendly name lookup first then FQCN.
- */
- private Class<?> resolveClass(String name) throws ClassNotFoundException {
- return org.apache.tika.serialization.ComponentNameResolver
- .resolveClass(name, classLoader);
- }
-
- /**
- * Creates a DefaultEncodingDetector that loads detectors from SPI, excluding the specified classes.
- *
- * @param excludeClasses encoding detector classes to exclude from SPI loading
- * @return the DefaultEncodingDetector with SPI-loaded detectors
- */
- private DefaultEncodingDetector createDefaultEncodingDetector(
- Collection<Class<? extends EncodingDetector>> excludeClasses) {
- return new DefaultEncodingDetector(new org.apache.tika.config.ServiceLoader(classLoader),
- excludeClasses);
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java
deleted file mode 100644
index 5d1f60cdc..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/ParserLoader.java
+++ /dev/null
@@ -1,311 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.config.loader;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.ServiceLoader;
-import java.util.Set;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.tika.detect.EncodingDetector;
-import org.apache.tika.exception.TikaConfigException;
-import org.apache.tika.mime.MediaType;
-import org.apache.tika.parser.AbstractEncodingDetectorParser;
-import org.apache.tika.parser.CompositeParser;
-import org.apache.tika.parser.Parser;
-import org.apache.tika.parser.ParserDecorator;
-import org.apache.tika.parser.RenderingParser;
-import org.apache.tika.renderer.Renderer;
-
-/**
- * Loader for parsers with support for decoration (mime type filtering).
- */
-public class ParserLoader {
-
- private static final Logger LOG = LoggerFactory.getLogger(ParserLoader.class);
-
- private final ClassLoader classLoader;
- private final ObjectMapper objectMapper;
- private final EncodingDetector encodingDetector;
- private final Renderer renderer;
-
- /**
- * Holds parsed config data before decoration is applied.
- */
- private static class ParsedParserConfig {
- final String name;
- final Parser parser;
- final FrameworkConfig.ParserDecoration decoration;
-
- ParsedParserConfig(String name, Parser parser,
- FrameworkConfig.ParserDecoration decoration) {
- this.name = name;
- this.parser = parser;
- this.decoration = decoration;
- }
- }
-
- public ParserLoader(ClassLoader classLoader, ObjectMapper objectMapper,
- EncodingDetector encodingDetector, Renderer renderer) {
- this.classLoader = classLoader;
- this.objectMapper = objectMapper;
- this.encodingDetector = encodingDetector;
- this.renderer = renderer;
- }
-
- /**
- * Loads parsers from JSON config and builds a CompositeParser.
- *
- * @param config the Tika JSON configuration
- * @return the composite parser
- * @throws TikaConfigException if loading fails
- */
- public CompositeParser load(TikaJsonConfig config) throws TikaConfigException {
- //TODO -- need to handle multiparsers at some point
- //TODO -- add special handling for external parsers?
- List<Parser> parserList = new ArrayList<>();
-
- // Load configured parsers
- if (config.hasComponentSection("parsers")) {
- List<Map.Entry<String, JsonNode>> parsers = config.getArrayComponents("parsers");
-
- // Check if "default-parser" is in the list and extract exclusions and decorations
- boolean hasDefaultParser = false;
- Set<Class<?>> excludedParserClasses = new HashSet<>();
- FrameworkConfig.ParserDecoration defaultParserDecoration = null;
-
- for (Map.Entry<String, JsonNode> entry : parsers) {
- if ("default-parser".equals(entry.getKey())) {
- hasDefaultParser = true;
-
- // Parse exclusions from default-parser config
- JsonNode configNode = entry.getValue();
-
- // Check for common mistake: using "_excludes" instead of "_exclude"
- if (configNode != null && configNode.has("_excludes")) {
- throw new TikaConfigException(
- "Invalid configuration for default-parser: found '_excludes' but the correct " +
- "field name is '_exclude' (singular). Please change '_excludes' to '_exclude' " +
- "in your configuration.");
- }
-
- if (configNode != null && configNode.has("_exclude")) {
- JsonNode excludeNode = configNode.get("_exclude");
- if (excludeNode.isArray()) {
- for (JsonNode excludeName : excludeNode) {
- if (excludeName.isTextual()) {
- String parserName = excludeName.asText();
- try {
- Class<?> parserClass = resolveClass(parserName);
- excludedParserClasses.add(parserClass);
- LOG.debug("Excluding parser from SPI: {}", parserName);
- } catch (Exception e) {
- LOG.warn("Unknown parser in default-parser exclude list: {}", parserName);
- }
- }
- }
- }
- }
-
- // Extract decoration (mimeInclude/mimeExclude) for default-parser
- if (configNode != null) {
- try {
- FrameworkConfig frameworkConfig = FrameworkConfig.extract(configNode, objectMapper);
- defaultParserDecoration = frameworkConfig.getDecoration();
- } catch (Exception e) {
- LOG.warn("Failed to extract decoration from default-parser: {}", e.getMessage());
- }
- }
- break;
- }
- }
-
- // First pass: parse configs and instantiate parsers
- // Skip "default-parser" - it's a special marker for SPI fallback, not a real parser
- Map<String, ParsedParserConfig> parsedConfigs = new LinkedHashMap<>();
- for (Map.Entry<String, JsonNode> entry : parsers) {
- String name = entry.getKey();
-
- // Skip the special "default-parser" marker
- if ("default-parser".equals(name)) {
- continue;
- }
-
- JsonNode configNode = entry.getValue();
- ParsedParserConfig parsed = loadConfiguredParser(name, configNode);
- parsedConfigs.put(name, parsed);
- }
-
- // Track configured parser classes (before decoration) to avoid SPI duplicates
- Set<Class<?>> configuredParserClasses = new HashSet<>();
- for (ParsedParserConfig parsed : parsedConfigs.values()) {
- configuredParserClasses.add(parsed.parser.getClass());
- }
-
- // Add excluded parsers to the configured set so they won't be loaded from SPI
- configuredParserClasses.addAll(excludedParserClasses);
-
- // Second pass: apply decorations that may reference other parsers
- for (ParsedParserConfig parsed : parsedConfigs.values()) {
- Parser parser = parsed.parser;
-
- // Apply decorations if present
- if (parsed.decoration != null) {
- // Apply mime type filtering
- if (parsed.decoration.hasFiltering()) {
- parser = applyMimeFiltering(parser, parsed.decoration);
- }
- }
-
- parserList.add(parser);
- }
-
- // Add SPI-discovered parsers only if "default-parser" is in config
- // If "default-parser" is present, use SPI fallback for unlisted parsers
- // If "default-parser" is NOT present, only load explicitly configured parsers
- if (hasDefaultParser) {
- List<Parser> spiParsers = loadSpiParsers(configuredParserClasses);
-
- // Apply decoration to SPI parsers if specified on default-parser
- if (defaultParserDecoration != null && defaultParserDecoration.hasFiltering()) {
- // Wrap SPI parsers in a CompositeParser and apply decoration
- Parser spiComposite = new CompositeParser(TikaLoader.getMediaTypeRegistry(), spiParsers);
- spiComposite = applyMimeFiltering(spiComposite, defaultParserDecoration);
- parserList.add(spiComposite);
- } else {
- parserList.addAll(spiParsers);
- }
- LOG.debug("Loading SPI parsers because 'default-parser' is in config");
- } else {
- LOG.debug("Skipping SPI parsers - 'default-parser' not in config");
- }
- } else {
- // No configured parsers - load all from SPI
- List<Parser> spiParsers = loadSpiParsers(Collections.emptySet());
- parserList.addAll(spiParsers);
- }
-
- return new CompositeParser(TikaLoader.getMediaTypeRegistry(), parserList);
- }
-
- private ParsedParserConfig loadConfiguredParser(String name, JsonNode configNode)
- throws TikaConfigException {
- try {
- // Extract framework config (decorations like mimeInclude/mimeExclude)
- FrameworkConfig frameworkConfig = FrameworkConfig.extract(configNode, objectMapper);
-
- // Use Jackson with mixins to deserialize - the TypeIdResolver handles name resolution
- Parser parser = deserializeParser(name, frameworkConfig.getComponentConfigNode());
-
- // Post-process: inject EncodingDetector for AbstractEncodingDetectorParser
- if (parser instanceof AbstractEncodingDetectorParser) {
- ((AbstractEncodingDetectorParser) parser).setEncodingDetector(encodingDetector);
- }
-
- // Post-process: inject Renderer for RenderingParser
- if (parser instanceof RenderingParser && renderer != null) {
- ((RenderingParser) parser).setRenderer(renderer);
- }
-
- return new ParsedParserConfig(name, parser, frameworkConfig.getDecoration());
-
- } catch (TikaConfigException e) {
- throw e;
- } catch (Exception e) {
- throw new TikaConfigException("Failed to load parser '" + name + "'", e);
- }
- }
-
- /**
- * Deserializes a parser, trying JsonConfig constructor first, then Jackson bean deserialization.
- */
- private Parser deserializeParser(String name, JsonNode configNode) throws TikaConfigException {
- return ComponentInstantiator.instantiate(name, configNode, objectMapper, classLoader);
- }
-
- /**
- * Resolves a name to a class, trying friendly name lookup first then FQCN.
- */
- private Class<?> resolveClass(String name) throws ClassNotFoundException {
- return org.apache.tika.serialization.ComponentNameResolver
- .resolveClass(name, classLoader);
- }
-
- private Parser applyMimeFiltering(Parser parser, FrameworkConfig.ParserDecoration decoration) {
- List<String> includes = decoration.getMimeInclude();
- List<String> excludes = decoration.getMimeExclude();
-
- if (!includes.isEmpty()) {
- Set<MediaType> includeTypes = new HashSet<>();
- for (String mimeStr : includes) {
- includeTypes.add(MediaType.parse(mimeStr));
- }
- parser = ParserDecorator.withTypes(parser, includeTypes);
- }
-
- if (!excludes.isEmpty()) {
- Set<MediaType> excludeTypes = new HashSet<>();
- for (String mimeStr : excludes) {
- excludeTypes.add(MediaType.parse(mimeStr));
- }
- parser = ParserDecorator.withoutTypes(parser, excludeTypes);
- }
-
- return parser;
- }
-
- private List<Parser> loadSpiParsers(Set<Class<?>> excludeClasses) {
- List<Parser> result = new ArrayList<>();
- ServiceLoader<Parser> serviceLoader = ServiceLoader.load(Parser.class, classLoader);
-
- Iterator<Parser> iterator = serviceLoader.iterator();
- while (iterator.hasNext()) {
- try {
- Parser parser = iterator.next();
-
- // Skip if this parser class was already loaded from config
- if (excludeClasses.contains(parser.getClass())) {
- LOG.debug("Skipping SPI parser {} - already configured",
- parser.getClass().getName());
- continue;
- }
- if (AbstractEncodingDetectorParser.class.isAssignableFrom(parser.getClass())) {
- ((AbstractEncodingDetectorParser)parser).setEncodingDetector(encodingDetector);
- }
- if (parser instanceof RenderingParser && renderer != null) {
- ((RenderingParser) parser).setRenderer(renderer);
- }
- result.add(parser);
- } catch (Exception e) {
- // Log and skip problematic SPI providers
- LOG.warn("Failed to load SPI parser: {}", e.getMessage(), e);
- }
- }
-
- return result;
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
index b82beb7d8..5748d894b 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaLoader.java
@@ -16,16 +16,27 @@
*/
package org.apache.tika.config.loader;
+import java.io.File;
import java.io.IOException;
+import java.io.OutputStream;
import java.nio.file.Path;
-import java.util.Collections;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.tika.config.GlobalSettings;
+import org.apache.tika.config.ServiceLoader;
+import org.apache.tika.detect.CompositeDetector;
+import org.apache.tika.detect.DefaultDetector;
+import org.apache.tika.detect.DefaultEncodingDetector;
import org.apache.tika.detect.Detector;
import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.exception.TikaConfigException;
@@ -38,9 +49,12 @@ import org.apache.tika.mime.MimeTypes;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.AutoDetectParserConfig;
import org.apache.tika.parser.CompositeParser;
+import org.apache.tika.parser.DefaultParser;
import org.apache.tika.parser.Parser;
import org.apache.tika.renderer.CompositeRenderer;
import org.apache.tika.renderer.Renderer;
+import org.apache.tika.serialization.ComponentConfig;
+import org.apache.tika.serialization.ComponentNameResolver;
import org.apache.tika.serialization.JsonMetadata;
import org.apache.tika.serialization.JsonMetadataList;
@@ -76,20 +90,65 @@ import org.apache.tika.serialization.JsonMetadataList;
*/
public class TikaLoader {
+ // Static registration of component configurations
+ static {
+ registerComponentConfigs();
+ }
+
+ private static void registerComponentConfigs() {
+ // Register how each component type should be loaded
+ ComponentConfig.builder("parsers", Parser.class)
+ .loadAsList()
+ .wrapWith(list -> new CompositeParser(getMediaTypeRegistry(), (List<Parser>) list))
+ .defaultProvider(DefaultParser::new)
+ .register();
+
+ ComponentConfig.builder("detectors", Detector.class)
+ .loadAsList()
+ .wrapWith(list -> new CompositeDetector(getMediaTypeRegistry(), (List<Detector>) list))
+ .defaultProvider(DefaultDetector::new)
+ .register();
+
+ ComponentConfig.builder("encoding-detectors", EncodingDetector.class)
+ .loadAsList()
+ .wrapWith(list -> list.isEmpty()
+ ? new DefaultEncodingDetector()
+ : new org.apache.tika.detect.CompositeEncodingDetector((List<EncodingDetector>) list))
+ .defaultProvider(DefaultEncodingDetector::new)
+ .register();
+
+ ComponentConfig.builder("metadata-filters", MetadataFilter.class)
+ .loadAsList()
+ .wrapWith(list -> list.isEmpty()
+ ? NoOpFilter.NOOP_FILTER
+ : new CompositeMetadataFilter((List<MetadataFilter>) list))
+ .defaultProvider(() -> NoOpFilter.NOOP_FILTER)
+ .register();
+
+ ComponentConfig.builder("renderers", Renderer.class)
+ .loadAsList()
+ .wrapWith(list -> new CompositeRenderer((List<Renderer>) list))
+ .register();
+
+ ComponentConfig.builder("translator", Translator.class)
+ .loadAsList()
+ .wrapWith(list -> list.isEmpty() ? null : (Translator) list.get(0))
+ .register();
+ }
+
private final TikaJsonConfig config;
private final ClassLoader classLoader;
private final ObjectMapper objectMapper;
- // Cached instances (lazy loaded)
+ // Cache of loaded components (keyed by component class)
+ private final Map<Class<?>, Object> componentCache = new ConcurrentHashMap<>();
+
+ // Static instances
private static MimeTypes mimeTypes;
private static TikaLoader defaultLoader;
- private Parser parsers;
+
+ // Special cached instances that aren't standard components
private Parser autoDetectParser;
- private Detector detectors;
- private EncodingDetector encodingDetectors;
- private MetadataFilter metadataFilter;
- private Renderer renderers;
- private Translator translator;
private ConfigLoader configLoader;
private GlobalSettings globalSettings;
@@ -173,136 +232,98 @@ public class TikaLoader {
/**
* Loads and returns all parsers.
+ * Syntactic sugar for {@code get(Parser.class)}.
* Results are cached - subsequent calls return the same instance.
- * <p>
- * Note: This method ensures EncodingDetectors are loaded first,
- * as some parsers require them during construction (e.g., AbstractEncodingDetectorParser
- * requires an EncodingDetector).
*
* @return the parser (typically a CompositeParser internally)
* @throws TikaConfigException if loading fails
*/
- public synchronized Parser loadParsers() throws TikaConfigException {
- if (parsers == null) {
- // Load EncodingDetectors first - some parsers need them during construction
- EncodingDetector encodingDetector = loadEncodingDetectors();
-
- // Load Renderers - some parsers need them during construction
- Renderer renderer = loadRenderers();
-
- ParserLoader loader = new ParserLoader(classLoader, objectMapper, encodingDetector, renderer);
- parsers = loader.load(config);
+ public Parser loadParsers() throws TikaConfigException {
+ try {
+ return get(Parser.class);
+ } catch (IOException e) {
+ throw new TikaConfigException("Failed to load parsers", e);
}
- return parsers;
}
/**
* Loads and returns all detectors.
- * Supports "default-detector" marker for SPI fallback with optional exclusions.
- * If "detectors" section exists:
- * - If "default-detector" is present: loads configured detectors + SPI detectors (minus exclusions)
- * - If "default-detector" is absent: loads only configured detectors (no SPI)
- * If "detectors" section missing: uses SPI to discover all detectors.
+ * Syntactic sugar for {@code get(Detector.class)}.
* Results are cached - subsequent calls return the same instance.
*
* @return the detector (typically a CompositeDetector internally)
* @throws TikaConfigException if loading fails
*/
- public synchronized Detector loadDetectors() throws TikaConfigException {
- if (detectors == null) {
- DetectorLoader loader = new DetectorLoader(classLoader, objectMapper);
- detectors = loader.load(config);
+ public Detector loadDetectors() throws TikaConfigException {
+ try {
+ return get(Detector.class);
+ } catch (IOException e) {
+ throw new TikaConfigException("Failed to load detectors", e);
}
- return detectors;
}
/**
* Loads and returns all encoding detectors.
- * Supports "default-encoding-detector" marker for SPI fallback with optional exclusions.
- * If "encoding-detectors" section exists:
- * - If "default-encoding-detector" is present: loads configured detectors + SPI detectors (minus exclusions)
- * - If "default-encoding-detector" is absent: loads only configured detectors (no SPI)
- * If "encoding-detectors" section missing: uses SPI to discover encoding detectors.
+ * Syntactic sugar for {@code get(EncodingDetector.class)}.
* Results are cached - subsequent calls return the same instance.
*
* @return the encoding detector (typically a CompositeEncodingDetector internally)
* @throws TikaConfigException if loading fails
*/
- public synchronized EncodingDetector loadEncodingDetectors() throws TikaConfigException {
- if (encodingDetectors == null) {
- EncodingDetectorLoader loader = new EncodingDetectorLoader(classLoader, objectMapper);
- encodingDetectors = loader.load(config);
+ public EncodingDetector loadEncodingDetectors() throws TikaConfigException {
+ try {
+ return get(EncodingDetector.class);
+ } catch (IOException e) {
+ throw new TikaConfigException("Failed to load encoding detectors", e);
}
- return encodingDetectors;
}
/**
* Loads and returns all metadata filters.
- * Metadata filters are opt-in only - they are NOT loaded from SPI by default.
- * If "metadata-filters" section exists in config, uses only those listed.
- * If section missing, returns an empty filter (no SPI fallback).
+ * Syntactic sugar for {@code get(MetadataFilter.class)}.
* Results are cached - subsequent calls return the same instance.
*
* @return the metadata filter (typically a CompositeMetadataFilter internally)
* @throws TikaConfigException if loading fails
*/
- public synchronized MetadataFilter loadMetadataFilters() throws TikaConfigException {
- if (metadataFilter == null) {
- List<MetadataFilter> filterList;
-
- // Check if metadata-filters section exists in config
- if (config.hasComponentSection("metadata-filters")) {
- // Load explicitly configured filters (no SPI fallback)
- CompositeComponentLoader<MetadataFilter> loader = new CompositeComponentLoader<>(
- MetadataFilter.class, "metadata-filters", classLoader, objectMapper);
- filterList = loader.loadFromArray(config);
- } else {
- // No config section - metadata filters are opt-in only, don't load from SPI
- filterList = Collections.emptyList();
- }
- if (filterList.isEmpty()) {
- metadataFilter = NoOpFilter.NOOP_FILTER;
- } else {
- metadataFilter = new CompositeMetadataFilter(filterList);
- }
+ public MetadataFilter loadMetadataFilters() throws TikaConfigException {
+ try {
+ return get(MetadataFilter.class);
+ } catch (IOException e) {
+ throw new TikaConfigException("Failed to load metadata filters", e);
}
- return metadataFilter;
}
/**
* Loads and returns all renderers.
- * If "renderers" section exists in config, uses only those listed (no SPI fallback).
- * If section missing, uses SPI to discover renderers.
+ * Syntactic sugar for {@code get(Renderer.class)}.
* Results are cached - subsequent calls return the same instance.
*
* @return the renderer (typically a CompositeRenderer internally)
* @throws TikaConfigException if loading fails
*/
- public synchronized Renderer loadRenderers() throws TikaConfigException {
- if (renderers == null) {
- CompositeComponentLoader<Renderer> loader = new CompositeComponentLoader<>(
- Renderer.class, "renderers", classLoader, objectMapper);
- List<Renderer> rendererList = loader.loadFromArray(config);
- renderers = new CompositeRenderer(rendererList);
+ public Renderer loadRenderers() throws TikaConfigException {
+ try {
+ return get(Renderer.class);
+ } catch (IOException e) {
+ throw new TikaConfigException("Failed to load renderers", e);
}
- return renderers;
}
/**
* Loads and returns the translator.
- * If "translator" section exists in config, uses that translator.
- * If section missing, uses SPI to discover translator.
+ * Syntactic sugar for {@code get(Translator.class)}.
* Results are cached - subsequent calls return the same instance.
*
* @return the translator
* @throws TikaConfigException if loading fails
*/
- public synchronized Translator loadTranslator() throws TikaConfigException {
- if (translator == null) {
- TranslatorLoader loader = new TranslatorLoader(classLoader, objectMapper);
- translator = loader.load(config);
+ public Translator loadTranslator() throws TikaConfigException {
+ try {
+ return get(Translator.class);
+ } catch (IOException e) {
+ throw new TikaConfigException("Failed to load translator", e);
}
- return translator;
}
/**
@@ -494,4 +515,337 @@ public class TikaLoader {
public GlobalSettings getGlobalSettings() {
return globalSettings;
}
+
+ // ==================== Generic Component Access ====================
+
+ /**
+ * Gets a component by its class type.
+ * Components are loaded lazily and cached.
+ *
+ * @param componentClass the component class (e.g., Parser.class, Detector.class)
+ * @return the loaded component
+ * @throws IOException if loading fails
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T get(Class<T> componentClass) throws IOException {
+ // Check cache first
+ if (componentCache.containsKey(componentClass)) {
+ return (T) componentCache.get(componentClass);
+ }
+
+ // Get component config from registry
+ ComponentConfig<T> componentConfig = ComponentNameResolver.getComponentConfig(componentClass);
+ if (componentConfig == null) {
+ throw new IllegalArgumentException(
+ "No component registered for class: " + componentClass.getName());
+ }
+
+ try {
+ // Load the component
+ T component = loadComponent(componentConfig);
+
+ // Cache and return
+ if (component != null) {
+ componentCache.put(componentClass, component);
+ }
+ return component;
+ } catch (TikaConfigException e) {
+ throw new IOException("Failed to load component: " + componentClass.getName(), e);
+ }
+ }
+
+ /**
+ * Gets a component by its JSON field name.
+ * Components are loaded lazily and cached.
+ *
+ * @param jsonField the JSON field name (e.g., "parsers", "detectors")
+ * @return the loaded component
+ * @throws IOException if loading fails
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T get(String jsonField) throws IOException {
+ // Get component config from registry by field name
+ ComponentConfig<?> componentConfig = ComponentNameResolver.getComponentConfig(jsonField);
+ if (componentConfig == null) {
+ throw new IllegalArgumentException("No component registered for field: " + jsonField);
+ }
+
+ // Delegate to get by class (which handles caching)
+ return (T) get(componentConfig.getComponentClass());
+ }
+
+ /**
+ * Generic component loading using ComponentConfig.
+ */
+ @SuppressWarnings("unchecked")
+ private <T> T loadComponent(ComponentConfig<T> componentConfig) throws TikaConfigException, IOException {
+ Class<T> componentClass = componentConfig.getComponentClass();
+
+ // Handle dependencies: Parsers need EncodingDetectors and Renderers loaded first
+ if (componentClass == Parser.class) {
+ get(EncodingDetector.class);
+ get(Renderer.class);
+ }
+
+ // Load the component list
+ List<T> componentList = loadComponentList(componentConfig.getJsonField(), componentClass);
+
+ // Apply post-processing (auto-exclusions for Parser/Detector)
+ componentList = applyPostProcessing(componentClass, componentList);
+
+ // If empty and has default, use default
+ if (componentList.isEmpty()) {
+ if (componentConfig.hasDefault()) {
+ return componentConfig.getDefault();
+ }
+ // For components that wrap empty lists
+ if (componentConfig.hasListWrapper()) {
+ return componentConfig.wrapList(componentList);
+ }
+ return null;
+ }
+
+ // Wrap the list if configured
+ if (componentConfig.hasListWrapper()) {
+ return componentConfig.wrapList(componentList);
+ }
+
+ // For single-value components
+ return componentList.isEmpty() ? null : componentList.get(0);
+ }
+
+ /**
+ * Applies post-processing to component lists.
+ * Currently handles auto-exclusions for Parser and Detector.
+ */
+ @SuppressWarnings("unchecked")
+ private <T> List<T> applyPostProcessing(Class<T> componentClass, List<T> list) {
+ if (componentClass == Parser.class) {
+ return (List<T>) applyParserAutoExclusions((List<Parser>) list);
+ } else if (componentClass == Detector.class) {
+ return (List<T>) applyDetectorAutoExclusions((List<Detector>) list);
+ }
+ return list;
+ }
+
+ // ==================== Component List Loading ====================
+
+ /**
+ * Loads a list of components from the JSON configuration.
+ *
+ * @param jsonField the JSON field name (e.g., "parsers", "detectors")
+ * @param componentClass the component class
+ * @return list of loaded components (may be empty, never null)
+ */
+ private <T> List<T> loadComponentList(String jsonField, Class<T> componentClass)
+ throws TikaConfigException {
+ List<Map.Entry<String, JsonNode>> entries = config.getArrayComponents(jsonField);
+
+ if (entries.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ List<T> components = new ArrayList<>();
+ for (Map.Entry<String, JsonNode> entry : entries) {
+ String typeName = entry.getKey();
+ JsonNode configNode = entry.getValue();
+
+ try {
+ // Create wrapper node: { "type-name": {...config...} }
+ ObjectNode wrapperNode = objectMapper.createObjectNode();
+ wrapperNode.set(typeName, configNode);
+
+ // Deserialize using Jackson (TikaModule handles type resolution)
+ T component = objectMapper.treeToValue(wrapperNode, componentClass);
+ components.add(component);
+ } catch (Exception e) {
+ throw new TikaConfigException(
+ "Failed to load " + componentClass.getSimpleName() + ": " + typeName, e);
+ }
+ }
+
+ return components;
+ }
+
+ // ==================== Auto-Exclusion Post-Processing ====================
+
+ /**
+ * Applies auto-exclusion to parsers: when both explicit parsers and DefaultParser
+ * are configured, the explicit parser classes are automatically excluded from
+ * DefaultParser's SPI loading to prevent duplicates.
+ */
+ @SuppressWarnings("unchecked")
+ private List<Parser> applyParserAutoExclusions(List<Parser> parsers) {
+ // Find all explicitly configured parser types (not DefaultParser)
+ Set<Class<?>> explicitTypes = new HashSet<>();
+ for (Parser p : parsers) {
+ if (!(p instanceof DefaultParser)) {
+ explicitTypes.add(p.getClass());
+ }
+ }
+
+ // If no explicit types, no auto-exclusions needed - but still preserve the list
+ if (explicitTypes.isEmpty()) {
+ return parsers;
+ }
+
+ // Recreate any DefaultParser with explicit types as auto-exclusions
+ List<Parser> adjusted = new ArrayList<>();
+ for (Parser p : parsers) {
+ if (p instanceof DefaultParser dp) {
+ // Combine explicit exclusions from config with auto-exclusions
+ Set<Class<? extends Parser>> combinedExclusions = new HashSet<>();
+
+ // Add config-specified exclusions
+ combinedExclusions.addAll(dp.getExcludedClasses());
+
+ // Add auto-exclusions (explicit parser types)
+ combinedExclusions.addAll((Set<Class<? extends Parser>>) (Set<?>) explicitTypes);
+
+ adjusted.add(new DefaultParser(
+ getMediaTypeRegistry(),
+ new ServiceLoader(classLoader),
+ combinedExclusions));
+ } else {
+ adjusted.add(p);
+ }
+ }
+ return adjusted;
+ }
+
+ /**
+ * Applies auto-exclusion to detectors: when both explicit detectors and DefaultDetector
+ * are configured, the explicit detector classes are automatically excluded from
+ * DefaultDetector's SPI loading to prevent duplicates.
+ */
+ @SuppressWarnings("unchecked")
+ private List<Detector> applyDetectorAutoExclusions(List<Detector> detectors) {
+ // Find all explicitly configured detector types (not DefaultDetector)
+ Set<Class<?>> explicitTypes = new HashSet<>();
+ for (Detector d : detectors) {
+ if (!(d instanceof DefaultDetector)) {
+ explicitTypes.add(d.getClass());
+ }
+ }
+
+ // If no explicit types, no auto-exclusions needed - but still preserve the list
+ if (explicitTypes.isEmpty()) {
+ return detectors;
+ }
+
+ // Recreate any DefaultDetector with explicit types as auto-exclusions
+ List<Detector> adjusted = new ArrayList<>();
+ for (Detector d : detectors) {
+ if (d instanceof DefaultDetector dd) {
+ // Combine explicit exclusions from config with auto-exclusions
+ Set<Class<? extends Detector>> combinedExclusions = new HashSet<>();
+
+ // Add config-specified exclusions
+ combinedExclusions.addAll(dd.getExcludedClasses());
+
+ // Add auto-exclusions (explicit detector types)
+ combinedExclusions.addAll((Set<Class<? extends Detector>>) (Set<?>) explicitTypes);
+
+ adjusted.add(new DefaultDetector(
+ getMimeTypes(),
+ new ServiceLoader(classLoader),
+ combinedExclusions));
+ } else {
+ adjusted.add(d);
+ }
+ }
+ return adjusted;
+ }
+
+ // ==================== Serialization ====================
+
+ /**
+ * Saves the current configuration to a JSON file (pretty-printed).
+ */
+ public void save(File file) throws IOException {
+ objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, buildOutputNode());
+ }
+
+ /**
+ * Saves the current configuration to an output stream (pretty-printed).
+ */
+ public void save(OutputStream outputStream) throws IOException {
+ objectMapper.writerWithDefaultPrettyPrinter().writeValue(outputStream, buildOutputNode());
+ }
+
+ /**
+ * Converts the current configuration to a JSON string (pretty-printed).
+ */
+ public String toJson() throws IOException {
+ return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(buildOutputNode());
+ }
+
+ private ObjectNode buildOutputNode() throws IOException {
+ ObjectNode output = objectMapper.createObjectNode();
+
+ // Serialize loaded components from cache
+ if (componentCache.containsKey(Parser.class)) {
+ output.set("parsers", serializeComponent(componentCache.get(Parser.class), "parsers"));
+ } else if (config.hasArrayComponents("parsers")) {
+ output.set("parsers", config.getRootNode().get("parsers"));
+ }
+
+ if (componentCache.containsKey(Detector.class)) {
+ output.set("detectors", serializeComponent(componentCache.get(Detector.class), "detectors"));
+ } else if (config.hasArrayComponents("detectors")) {
+ output.set("detectors", config.getRootNode().get("detectors"));
+ }
+
+ if (componentCache.containsKey(EncodingDetector.class)) {
+ output.set("encoding-detectors", serializeComponent(componentCache.get(EncodingDetector.class), "encoding-detectors"));
+ } else if (config.hasArrayComponents("encoding-detectors")) {
+ output.set("encoding-detectors", config.getRootNode().get("encoding-detectors"));
+ }
+
+ Object metadataFilter = componentCache.get(MetadataFilter.class);
+ if (metadataFilter != null && metadataFilter != NoOpFilter.NOOP_FILTER) {
+ output.set("metadata-filters", serializeComponent(metadataFilter, "metadata-filters"));
+ } else if (config.hasArrayComponents("metadata-filters")) {
+ output.set("metadata-filters", config.getRootNode().get("metadata-filters"));
+ }
+
+ if (componentCache.containsKey(Renderer.class)) {
+ output.set("renderers", serializeComponent(componentCache.get(Renderer.class), "renderers"));
+ } else if (config.hasArrayComponents("renderers")) {
+ output.set("renderers", config.getRootNode().get("renderers"));
+ }
+
+ // Preserve auto-detect-parser config if present
+ JsonNode adpNode = config.getRootNode().get("auto-detect-parser");
+ if (adpNode != null && !adpNode.isNull()) {
+ output.set("auto-detect-parser", adpNode);
+ }
+
+ return output;
+ }
+
+ private JsonNode serializeComponent(Object component, String jsonField) throws IOException {
+ Object toSerialize = unwrapForSerialization(component);
+ if (toSerialize == null) {
+ return objectMapper.createArrayNode();
+ }
+ return objectMapper.valueToTree(toSerialize);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Object unwrapForSerialization(Object component) {
+ if (component instanceof CompositeParser cp) {
+ Map<org.apache.tika.mime.MediaType, Parser> parserMap = cp.getParsers();
+ // Get unique parsers from the map
+ return new ArrayList<>(new HashSet<>(parserMap.values()));
+ } else if (component instanceof CompositeDetector cd) {
+ return cd.getDetectors();
+ } else if (component instanceof CompositeMetadataFilter cmf) {
+ return cmf.getFilters();
+ } else if (component instanceof org.apache.tika.detect.CompositeEncodingDetector ced) {
+ return ced.getDetectors();
+ }
+ // For types without accessor methods (like CompositeRenderer), return as-is
+ return component;
+ }
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
index 93716884d..6c6521a87 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/TikaObjectMapperFactory.java
@@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.serialization.ComponentNameResolver;
-import org.apache.tika.serialization.TikaAbstractTypeMixins;
+import org.apache.tika.serialization.TikaModule;
/**
* Factory for creating ObjectMappers configured for Tika serialization.
@@ -85,8 +85,12 @@ public class TikaObjectMapperFactory {
// Load component registries for name resolution
loadComponentRegistries();
- // Register deserializers for abstract types using wrapper object format
- TikaAbstractTypeMixins.registerDeserializers(mapper);
+ // Register TikaModule for compact serialization/deserialization of registered components
+ TikaModule tikaModule = new TikaModule();
+ mapper.registerModule(tikaModule);
+
+ // Set the shared mapper for TikaModule's deserializers
+ TikaModule.setSharedMapper(mapper);
return mapper;
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/TranslatorLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/TranslatorLoader.java
deleted file mode 100644
index 14f364aea..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/TranslatorLoader.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.config.loader;
-
-import java.util.Iterator;
-import java.util.Map;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.tika.exception.TikaConfigException;
-import org.apache.tika.language.translate.DefaultTranslator;
-import org.apache.tika.language.translate.Translator;
-
-/**
- * Loader for translators.
- * Only one translator is supported at a time.
- * <p>
- * JSON format uses wrapper object style:
- * <pre>
- * {
- * "translator": {
- * "google-translator": {
- * "apiKey": "..."
- * }
- * }
- * }
- * </pre>
- */
-public class TranslatorLoader {
-
- private static final Logger LOG = LoggerFactory.getLogger(TranslatorLoader.class);
-
- private final ClassLoader classLoader;
- private final ObjectMapper objectMapper;
-
- public TranslatorLoader(ClassLoader classLoader, ObjectMapper objectMapper) {
- this.classLoader = classLoader;
- this.objectMapper = objectMapper;
- }
-
- /**
- * Loads a translator from JSON config.
- * <p>
- * If "translator" section exists in config, uses that translator.
- * If section missing, uses DefaultTranslator to discover translator via SPI.
- *
- * @param config the Tika JSON configuration
- * @return the translator
- * @throws TikaConfigException if loading fails
- */
- public Translator load(TikaJsonConfig config) throws TikaConfigException {
- // Check if translator section exists in config
- if (config.hasComponentSection("translator")) {
- JsonNode translatorNode = config.getRootNode().get("translator");
- return loadConfiguredTranslator(translatorNode);
- } else {
- // No configured translator - use DefaultTranslator to load from SPI
- return createDefaultTranslator();
- }
- }
-
- private Translator loadConfiguredTranslator(JsonNode translatorNode)
- throws TikaConfigException {
- if (!translatorNode.isObject() || translatorNode.isEmpty()) {
- throw new TikaConfigException(
- "Translator configuration must be an object with translator type as key");
- }
-
- // Get the single field name (translator type) and its config
- Iterator<Map.Entry<String, JsonNode>> properties = translatorNode.properties().iterator();
- Map.Entry<String, JsonNode> entry = properties.next();
-
- if (properties.hasNext()) {
- throw new TikaConfigException(
- "Translator configuration must have exactly one translator type");
- }
-
- String typeName = entry.getKey();
- JsonNode configNode = entry.getValue();
-
- return deserializeTranslator(typeName, configNode);
- }
-
- /**
- * Deserializes a translator, trying JsonConfig constructor first, then Jackson bean deserialization.
- */
- private Translator deserializeTranslator(String name, JsonNode configNode)
- throws TikaConfigException {
- return ComponentInstantiator.instantiate(name, configNode, objectMapper, classLoader);
- }
-
- /**
- * Creates a DefaultTranslator that loads a translator from SPI.
- *
- * @return the DefaultTranslator with SPI-loaded translator
- */
- private DefaultTranslator createDefaultTranslator() {
- return new DefaultTranslator(new org.apache.tika.config.ServiceLoader(classLoader));
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentConfig.java b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentConfig.java
new file mode 100644
index 000000000..bc2b7fef1
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentConfig.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Configuration for how to load a top-level component from JSON.
+ * <p>
+ * Specifies:
+ * <ul>
+ * <li>The JSON field name (e.g., "parsers", "detectors")</li>
+ * <li>Whether to load as a list or single object</li>
+ * <li>How to wrap a list into a composite object (e.g., List<Parser> β CompositeParser)</li>
+ * <li>What default to return if the field is absent</li>
+ * </ul>
+ *
+ * @param <T> the component type (e.g., Parser, Detector)
+ */
+public class ComponentConfig<T> {
+
+ private final String jsonField;
+ private final Class<T> componentClass;
+ private final boolean loadAsList;
+ private final Function<List<?>, T> listWrapper;
+ private final Supplier<T> defaultProvider;
+
+ private ComponentConfig(Builder<T> builder) {
+ this.jsonField = builder.jsonField;
+ this.componentClass = builder.componentClass;
+ this.loadAsList = builder.loadAsList;
+ this.listWrapper = builder.listWrapper;
+ this.defaultProvider = builder.defaultProvider;
+ }
+
+ public String getJsonField() {
+ return jsonField;
+ }
+
+ public Class<T> getComponentClass() {
+ return componentClass;
+ }
+
+ public boolean isLoadAsList() {
+ return loadAsList;
+ }
+
+ @SuppressWarnings("unchecked")
+ public T wrapList(List<?> list) {
+ if (listWrapper == null) {
+ throw new IllegalStateException("No list wrapper configured for " + jsonField);
+ }
+ return listWrapper.apply(list);
+ }
+
+ public boolean hasListWrapper() {
+ return listWrapper != null;
+ }
+
+ public T getDefault() {
+ return defaultProvider != null ? defaultProvider.get() : null;
+ }
+
+ public boolean hasDefault() {
+ return defaultProvider != null;
+ }
+
+ /**
+ * Creates a new builder for ComponentConfig.
+ *
+ * @param jsonField the JSON field name (e.g., "parsers")
+ * @param componentClass the component interface (e.g., Parser.class)
+ * @return a new builder
+ */
+ public static <T> Builder<T> builder(String jsonField, Class<T> componentClass) {
+ return new Builder<>(jsonField, componentClass);
+ }
+
+ /**
+ * Builder for ComponentConfig.
+ */
+ public static class Builder<T> {
+ private final String jsonField;
+ private final Class<T> componentClass;
+ private boolean loadAsList = false;
+ private Function<List<?>, T> listWrapper;
+ private Supplier<T> defaultProvider;
+
+ Builder(String jsonField, Class<T> componentClass) {
+ this.jsonField = jsonField;
+ this.componentClass = componentClass;
+ }
+
+ /**
+ * Configure this component to be loaded as a list from JSON.
+ */
+ public Builder<T> loadAsList() {
+ this.loadAsList = true;
+ return this;
+ }
+
+ /**
+ * Configure how to wrap a list into the component type.
+ * For example, List<Parser> β CompositeParser.
+ *
+ * @param wrapper function that takes a list and returns the wrapped component
+ */
+ public Builder<T> wrapWith(Function<List<?>, T> wrapper) {
+ this.listWrapper = wrapper;
+ return this;
+ }
+
+ /**
+ * Configure a default value to return when the JSON field is absent.
+ *
+ * @param provider supplier that creates the default instance
+ */
+ public Builder<T> defaultProvider(Supplier<T> provider) {
+ this.defaultProvider = provider;
+ return this;
+ }
+
+ /**
+ * Build the ComponentConfig.
+ */
+ public ComponentConfig<T> build() {
+ return new ComponentConfig<>(this);
+ }
+
+ /**
+ * Build and register with ComponentNameResolver.
+ */
+ public void register() {
+ ComponentNameResolver.registerComponentConfig(new ComponentConfig<>(this));
+ }
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
index 0b7d9a700..80c54c017 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ComponentNameResolver.java
@@ -16,8 +16,10 @@
*/
package org.apache.tika.serialization;
+import java.util.Collections;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.tika.config.loader.ComponentInfo;
@@ -30,11 +32,17 @@ import org.apache.tika.exception.TikaConfigException;
* Supports friendly names like "pdf-parser" as well as fully qualified class names.
* Registries must be registered via {@link #registerRegistry(String, ComponentRegistry)}
* before use.
+ * <p>
+ * Also stores {@link ComponentConfig} registrations for top-level component loading.
*/
public final class ComponentNameResolver {
private static final Map<String, ComponentRegistry> REGISTRIES = new ConcurrentHashMap<>();
+ // Component configuration storage (keyed by JSON field name and by component class)
+ private static final Map<String, ComponentConfig<?>> FIELD_TO_CONFIG = new ConcurrentHashMap<>();
+ private static final Map<Class<?>, ComponentConfig<?>> CLASS_TO_CONFIG = new ConcurrentHashMap<>();
+
private ComponentNameResolver() {
// Utility class
}
@@ -121,4 +129,78 @@ public final class ComponentNameResolver {
}
return Optional.empty();
}
+
+ /**
+ * Checks if any registered component implements or extends the given abstract type.
+ * <p>
+ * This is used by TikaModule to determine if an abstract type (interface or abstract class)
+ * should use compact component serialization.
+ *
+ * @param abstractType the abstract type to check
+ * @return true if at least one registered component is assignable to this type
+ */
+ public static boolean hasImplementationsOf(Class<?> abstractType) {
+ for (ComponentRegistry registry : REGISTRIES.values()) {
+ for (ComponentInfo info : registry.getAllComponents().values()) {
+ if (abstractType.isAssignableFrom(info.componentClass())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // ==================== Component Config Methods ====================
+
+ /**
+ * Registers a ComponentConfig for top-level component loading.
+ *
+ * @param config the component configuration
+ */
+ public static <T> void registerComponentConfig(ComponentConfig<T> config) {
+ FIELD_TO_CONFIG.put(config.getJsonField(), config);
+ CLASS_TO_CONFIG.put(config.getComponentClass(), config);
+ }
+
+ /**
+ * Gets component configuration by JSON field name.
+ *
+ * @param jsonField the JSON field name (e.g., "parsers")
+ * @return the component config, or null if not registered
+ */
+ public static ComponentConfig<?> getComponentConfig(String jsonField) {
+ return FIELD_TO_CONFIG.get(jsonField);
+ }
+
+ /**
+ * Gets component configuration by component class.
+ *
+ * @param componentClass the component class (e.g., Parser.class)
+ * @return the component config, or null if not registered
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> ComponentConfig<T> getComponentConfig(Class<T> componentClass) {
+ return (ComponentConfig<T>) CLASS_TO_CONFIG.get(componentClass);
+ }
+
+ /**
+ * Checks if a component config is registered for the given JSON field.
+ */
+ public static boolean hasComponentConfig(String jsonField) {
+ return FIELD_TO_CONFIG.containsKey(jsonField);
+ }
+
+ /**
+ * Checks if a component config is registered for the given class.
+ */
+ public static boolean hasComponentConfig(Class<?> componentClass) {
+ return CLASS_TO_CONFIG.containsKey(componentClass);
+ }
+
+ /**
+ * Gets all registered component JSON field names.
+ */
+ public static Set<String> getComponentFields() {
+ return Collections.unmodifiableSet(FIELD_TO_CONFIG.keySet());
+ }
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
index 507a18211..2be514b85 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ConfigDeserializer.java
@@ -20,19 +20,19 @@ import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.loader.JsonMergeUtils;
import org.apache.tika.config.loader.TikaObjectMapperFactory;
import org.apache.tika.parser.ParseContext;
/**
- * Helper utility for parsers to deserialize their configuration from ConfigContainer.
+ * Helper utility for {@link org.apache.tika.config.SelfConfiguring} components
+ * to deserialize their configuration from ParseContext at run time.
* <p>
* <strong>Note for Parser Developers:</strong> Instead of calling this class directly,
* use {@link org.apache.tika.config.ParseContextConfig} which provides the same functionality
* but with better error handling. ParseContextConfig will throw a clear exception if
- * ConfigContainer has config but tika-serialization is not on the classpath.
+ * tika-serialization is not on the classpath.
* <p>
* This allows parsers to retrieve their configuration using the same friendly names
* as in tika-config.json (e.g., "pdf-parser", "html-parser") from per-request
@@ -55,53 +55,65 @@ public class ConfigDeserializer {
private static final ObjectMapper MAPPER = TikaObjectMapperFactory.getMapper();
/**
- * Retrieves and deserializes a parser configuration from the ConfigContainer in ParseContext.
- * If a default config is provided, the user config will be merged on top of it.
+ * Retrieves and deserializes a configuration from ParseContext.
* <p>
- * The resolved config is also set directly in the ParseContext under the configClass key,
- * so other components (like renderers) can find it via {@code parseContext.get(configClass)}.
+ * Resolution order:
+ * <ol>
+ * <li>Check resolved configs cache (already deserialized)</li>
+ * <li>Check JSON configs (deserialize, merge with default, cache)</li>
+ * <li>Return default config if nothing found</li>
+ * </ol>
+ * <p>
+ * The resolved config is cached in ParseContext's resolvedConfigs map and also
+ * set in the main context map so components can find it via {@code parseContext.get(configClass)}.
*
- * @param context the parse context containing the ConfigContainer
- * @param configKey the configuration key (e.g., "pdf-parser", "html-parser")
- * @param configClass the configuration class to deserialize into
+ * @param context the parse context
+ * @param configKey the configuration key (e.g., "pdf-parser", "handler-config")
+ * @param configClass the configuration class to deserialize into
* @param defaultConfig optional default config to merge with user config (can be null)
- * @param <T> the configuration type
+ * @param <T> the configuration type
* @return the merged configuration, the default config if no user config found, or null if neither exists
* @throws IOException if deserialization fails
*/
+ @SuppressWarnings("unchecked")
public static <T> T getConfig(ParseContext context, String configKey, Class<T> configClass, T defaultConfig)
throws IOException {
if (context == null) {
return defaultConfig;
}
- ConfigContainer configContainer = context.get(ConfigContainer.class);
- if (configContainer == null) {
- return defaultConfig;
+ // Check resolved cache first
+ T resolved = context.getResolvedConfig(configKey);
+ if (resolved != null) {
+ return resolved;
}
- JsonConfig jsonConfig = configContainer.get(configKey).orElse(null);
+ // Check for JSON config
+ JsonConfig jsonConfig = context.getJsonConfig(configKey);
if (jsonConfig == null) {
return defaultConfig;
}
+ // Deserialize and merge with default
T config = JsonMergeUtils.mergeWithDefaults(MAPPER, jsonConfig.json(), configClass, defaultConfig);
- // Also set the resolved config directly in ParseContext so other components
- // (like renderers) can find it via parseContext.get(configClass)
+ // Cache in resolved configs
+ context.setResolvedConfig(configKey, config);
+
+ // Also set in main context so other components can find it via parseContext.get(configClass)
context.set(configClass, config);
return config;
}
/**
- * Retrieves and deserializes a parser configuration from the ConfigContainer in ParseContext.
+ * Retrieves and deserializes a configuration from ParseContext.
* This version does not merge with any default config.
*
- * @param context the parse context containing the ConfigContainer
- * @param configKey the configuration key (e.g., "pdf-parser", "html-parser")
+ * @param context the parse context
+ * @param configKey the configuration key (e.g., "pdf-parser", "handler-config")
* @param configClass the configuration class to deserialize into
- * @param <T> the configuration type
+ * @param <T> the configuration type
* @return the deserialized configuration, or null if not found
* @throws IOException if deserialization fails
*/
@@ -111,22 +123,16 @@ public class ConfigDeserializer {
}
/**
- * Checks if a configuration exists in the ConfigContainer.
+ * Checks if a configuration exists in the ParseContext.
*
- * @param context the parse context
+ * @param context the parse context
* @param configKey the configuration key to check
- * @return true if the configuration exists
+ * @return true if the configuration exists (either as JSON or already resolved)
*/
public static boolean hasConfig(ParseContext context, String configKey) {
if (context == null) {
return false;
}
-
- ConfigContainer configContainer = context.get(ConfigContainer.class);
- if (configContainer == null) {
- return false;
- }
-
- return configContainer.get(configKey).isPresent();
+ return context.hasJsonConfig(configKey) || context.getResolvedConfig(configKey) != null;
}
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadata.java b/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadata.java
index 504fb4f19..16607e2ad 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadata.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadata.java
@@ -27,6 +27,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.serialization.serdes.MetadataDeserializer;
+import org.apache.tika.serialization.serdes.MetadataSerializer;
public class JsonMetadata {
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadataList.java b/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadataList.java
index 7611cdfea..2571c4c4b 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadataList.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/JsonMetadataList.java
@@ -28,6 +28,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.serialization.serdes.MetadataDeserializer;
+import org.apache.tika.serialization.serdes.MetadataSerializer;
public class JsonMetadataList {
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextDeserializer.java
deleted file mode 100644
index d57578677..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextDeserializer.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.serialization;
-
-import static org.apache.tika.serialization.ParseContextSerializer.PARSE_CONTEXT;
-
-import java.io.IOException;
-import java.util.Iterator;
-import java.util.Optional;
-
-import com.fasterxml.jackson.core.JacksonException;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.tika.config.ConfigContainer;
-import org.apache.tika.config.SelfConfiguring;
-import org.apache.tika.config.loader.ComponentInfo;
-import org.apache.tika.config.loader.TikaObjectMapperFactory;
-import org.apache.tika.parser.ParseContext;
-
-/**
- * Deserializes ParseContext from JSON using friendly names.
- * <p>
- * Fields with friendly names are stored in ConfigContainer for later resolution.
- * Fields with full class names (org.apache.tika.*) are deserialized directly.
- * <p>
- * Example input:
- * <pre>
- * {
- * "pdf-parser": {"extractActions": true},
- * "tika-task-timeout": {"timeoutMillis": 5000}
- * }
- * </pre>
- */
-public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
-
- private static final Logger LOG = LoggerFactory.getLogger(ParseContextDeserializer.class);
- private static final ObjectMapper MAPPER = TikaObjectMapperFactory.getMapper();
-
- @Override
- public ParseContext deserialize(JsonParser jsonParser,
- DeserializationContext deserializationContext)
- throws IOException, JacksonException {
- JsonNode root = jsonParser.readValueAsTree();
- return readParseContext(root);
- }
-
- /**
- * Deserializes a ParseContext from a JsonNode.
- * <p>
- * Fields with friendly names are stored as JSON in ConfigContainer for later
- * resolution via {@link ParseContextUtils#resolveAll}.
- * <p>
- * Fields with full class names (org.apache.tika.*) are deserialized directly
- * into the ParseContext for immediate use.
- *
- * @param jsonNode the JSON node containing the ParseContext data
- * @return the deserialized ParseContext with ConfigContainer populated
- * @throws IOException if deserialization fails
- */
- public static ParseContext readParseContext(JsonNode jsonNode) throws IOException {
- // Some use cases include the wrapper node, e.g. { "parseContext": {}}
- // Some include the contents only.
- // Try to find "parseContext" to start. If that doesn't exist, assume jsonNode is the contents.
- JsonNode contextNode = jsonNode.get(PARSE_CONTEXT);
- if (contextNode == null) {
- contextNode = jsonNode;
- }
-
- ParseContext parseContext = new ParseContext();
-
- // Store all fields as named configurations in ConfigContainer
- // Resolution to actual objects happens via ParseContextUtils.resolveAll()
- ConfigContainer configContainer = null;
- for (Iterator<String> it = contextNode.fieldNames(); it.hasNext(); ) {
- String fieldName = it.next();
- JsonNode fieldValue = contextNode.get(fieldName);
-
- // Try to resolve fieldName - either as FQCN or friendly name from registry
- Class<?> keyClass = null;
-
- // Check if fieldName is a full class name (for directly serialized Tika types)
- if (fieldName.startsWith("org.apache.tika.")) {
- try {
- keyClass = Class.forName(fieldName);
- } catch (ClassNotFoundException e) {
- LOG.debug("Class not found for key '{}', will check registry", fieldName);
- }
- }
-
- // If not found as FQCN, check registry for friendly name
- // Use ComponentNameResolver to ensure consistency with TikaObjectMapperFactory's registries
- boolean isSelfConfiguring = false;
- Class<?> contextKey = null; // The key to use when adding to ParseContext
- if (keyClass == null) {
- Optional<ComponentInfo> infoOpt = ComponentNameResolver.getComponentInfo(fieldName);
- if (infoOpt.isPresent()) {
- ComponentInfo info = infoOpt.get();
- keyClass = info.componentClass();
- isSelfConfiguring = info.selfConfiguring();
- contextKey = info.contextKey();
- LOG.debug("Resolved friendly name '{}' to class {} (selfConfiguring={}, contextKey={})",
- fieldName, keyClass.getName(), isSelfConfiguring,
- contextKey != null ? contextKey.getName() : "null");
- }
- } else {
- // For FQCN resolution, check SelfConfiguring directly
- isSelfConfiguring = SelfConfiguring.class.isAssignableFrom(keyClass);
- }
-
- // If we found a class, check if it's SelfConfiguring
- if (keyClass != null) {
- // SelfConfiguring components (Parsers, Detectors, etc.) handle their own config
- // at runtime - keep their config in ConfigContainer for later access
- if (isSelfConfiguring) {
- LOG.debug("'{}' maps to SelfConfiguring class {}, keeping in ConfigContainer",
- fieldName, keyClass.getName());
- // Fall through to ConfigContainer storage below
- } else {
- // Non-SelfConfiguring - deserialize directly into ParseContext
- try {
- // Check if fieldValue is a wrapper object format: {"concrete-class": {props}}
- Object value;
- if (fieldValue.isObject() && fieldValue.size() == 1) {
- String typeName = fieldValue.fieldNames().next();
- JsonNode configNode = fieldValue.get(typeName);
- // Try to resolve the concrete class
- try {
- Class<?> concreteClass = ComponentNameResolver.resolveClass(typeName,
- ParseContextDeserializer.class.getClassLoader());
- value = MAPPER.treeToValue(configNode, concreteClass);
- } catch (ClassNotFoundException ex) {
- // Fall back to key class
- value = MAPPER.treeToValue(configNode, keyClass);
- }
- } else {
- // Not wrapper format, deserialize directly
- value = MAPPER.treeToValue(fieldValue, keyClass);
- }
- // Use contextKey if specified, otherwise use the component class
- Class<?> parseContextKey = (contextKey != null) ? contextKey : keyClass;
- parseContext.set((Class) parseContextKey, value);
- continue;
- } catch (Exception e) {
- throw new IOException("Failed to deserialize '" + fieldName + "': " + e.getMessage(), e);
- }
- }
- }
-
- // Store as config for later resolution
- if (configContainer == null) {
- configContainer = new ConfigContainer();
- }
- configContainer.set(fieldName, fieldValue.toString());
- }
-
- if (configContainer != null) {
- parseContext.set(ConfigContainer.class, configContainer);
- }
-
- // Resolve array configs (e.g., "metadata-filters") and non-SelfConfiguring components
- ParseContextUtils.resolveAll(parseContext, ParseContextDeserializer.class.getClassLoader());
-
- return parseContext;
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextSerializer.java
deleted file mode 100644
index e07559fab..000000000
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextSerializer.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.serialization;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.tika.config.ConfigContainer;
-import org.apache.tika.config.loader.TikaObjectMapperFactory;
-import org.apache.tika.parser.ParseContext;
-
-/**
- * Serializes ParseContext to JSON using friendly names.
- * <p>
- * Serializes:
- * <ul>
- * <li>ConfigContainer contents (JSON strings) - written as-is</li>
- * <li>Objects in ParseContext that have registered friendly names - serialized via Jackson</li>
- * </ul>
- * <p>
- * Example output:
- * <pre>
- * {
- * "pdf-parser": {"extractActions": true},
- * "tika-task-timeout": {"timeoutMillis": 5000},
- * "handler-config": {"type": "XML", "parseMode": "RMETA"}
- * }
- * </pre>
- */
-public class ParseContextSerializer extends JsonSerializer<ParseContext> {
-
- private static final Logger LOG = LoggerFactory.getLogger(ParseContextSerializer.class);
- public static final String PARSE_CONTEXT = "parseContext";
-
- // Full mapper with polymorphic type handling (includes WrapperObjectSerializer)
- private static final ObjectMapper MAPPER = TikaObjectMapperFactory.getMapper();
-
- // Plain mapper without WrapperObjectSerializer - for types with friendly names
- // where the wrapper is added at the field name level by this serializer
- private static final ObjectMapper PLAIN_MAPPER = new ObjectMapper();
-
- @Override
- public void serialize(ParseContext parseContext, JsonGenerator jsonGenerator,
- SerializerProvider serializerProvider) throws IOException {
- jsonGenerator.writeStartObject();
-
- Set<String> writtenKeys = new HashSet<>();
- ConfigContainer configContainer = parseContext.get(ConfigContainer.class);
-
- // First, write ConfigContainer contents (these are already JSON strings)
- if (configContainer != null) {
- for (String key : configContainer.getKeys()) {
- jsonGenerator.writeFieldName(key);
- jsonGenerator.writeRawValue(configContainer.get(key).get().json());
- writtenKeys.add(key);
- }
- }
-
- // Then, serialize objects from ParseContext that have registered friendly names
- // or are stored under Tika type keys (for polymorphic custom subclasses)
- Map<String, Object> contextMap = parseContext.getContextMap();
- for (Map.Entry<String, Object> entry : contextMap.entrySet()) {
- // Skip ConfigContainer - already handled above
- if (entry.getKey().equals(ConfigContainer.class.getName())) {
- continue;
- }
-
- Object value = entry.getValue();
- if (value == null) {
- continue;
- }
-
- // Try to get friendly name for this object's class
- // Use ComponentNameResolver to ensure consistency with TikaObjectMapperFactory's registries
- String friendlyName = ComponentNameResolver.getFriendlyName(value.getClass());
-
- // Determine key: prefer friendly name, fall back to FQCN for Tika types
- String key;
- if (friendlyName != null) {
- // Use friendly name if available (deserializer will resolve via registry)
- key = friendlyName;
- } else if (entry.getKey().startsWith("org.apache.tika.")) {
- // For Tika types without friendly names, use the context key (FQCN)
- key = entry.getKey();
- } else {
- // Skip non-Tika types without friendly names (e.g., String, custom non-Tika classes)
- continue;
- }
-
- if (!writtenKeys.contains(key)) {
- jsonGenerator.writeFieldName(key);
- if (friendlyName != null) {
- // Type has friendly name - use plain mapper to write properties directly
- // (key already serves as the type identifier)
- PLAIN_MAPPER.writeValue(jsonGenerator, value);
- } else {
- // No friendly name - add wrapper with FQCN and use MAPPER for
- // polymorphic type handling of nested types
- jsonGenerator.writeStartObject();
- jsonGenerator.writeFieldName(value.getClass().getName());
- MAPPER.writeValue(jsonGenerator, value);
- jsonGenerator.writeEndObject();
- }
- writtenKeys.add(key);
- }
- }
-
- jsonGenerator.writeEndObject();
- }
-}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
index 4638b160b..d31d5cd50 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
@@ -26,7 +26,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.JsonConfig;
import org.apache.tika.config.loader.ComponentInfo;
import org.apache.tika.config.loader.ComponentInstantiator;
@@ -53,7 +52,7 @@ import org.apache.tika.parser.ParseContext;
* </pre>
* <p>
* Components that implement {@link org.apache.tika.config.SelfConfiguring} are skipped
- * during resolution - they read their own config from ConfigContainer at runtime.
+ * during resolution - they read their own config from jsonConfigs at runtime.
*/
public class ParseContextUtils {
@@ -87,13 +86,13 @@ public class ParseContextUtils {
private record ArrayConfigInfo(Class<?> contextKey, Class<?> componentInterface) {}
/**
- * Resolves all friendly-named components from ConfigContainer and adds them to ParseContext.
+ * Resolves all JSON configs from ParseContext and adds them to the resolved cache.
* <p>
- * Iterates through all entries in ConfigContainer, looks up the friendly name in ComponentRegistry,
- * deserializes the JSON, and adds the instance to ParseContext.
+ * Iterates through all entries in jsonConfigs, looks up the friendly name in ComponentRegistry,
+ * deserializes the JSON, and caches the instance in resolvedConfigs.
* <p>
* Components that implement {@link org.apache.tika.config.SelfConfiguring} are skipped -
- * they read their own config from ConfigContainer at runtime.
+ * they read their own config at runtime via {@link ConfigDeserializer}.
* <p>
* The ParseContext key is determined by:
* <ol>
@@ -101,9 +100,6 @@ public class ParseContextUtils {
* <li>Auto-detected from {@link #KNOWN_CONTEXT_INTERFACES} (if component implements one)</li>
* <li>The component's own class (default)</li>
* </ol>
- * <p>
- * After resolution, resolved configs are removed from the ConfigContainer. If the
- * ConfigContainer becomes empty, it is removed from the ParseContext.
*
* @param context the ParseContext to populate
* @param classLoader the ClassLoader to use for loading component classes
@@ -113,20 +109,18 @@ public class ParseContextUtils {
return;
}
- ConfigContainer container = context.get(ConfigContainer.class);
- if (container == null) {
+ Map<String, JsonConfig> jsonConfigs = context.getJsonConfigs();
+ if (jsonConfigs.isEmpty()) {
return;
}
- List<String> resolvedKeys = new ArrayList<>();
-
// First, process known array configs (e.g., "metadata-filters")
// These don't depend on the other-configs registry
- for (String friendlyName : new ArrayList<>(container.getKeys())) {
+ for (String friendlyName : new ArrayList<>(jsonConfigs.keySet())) {
if (ARRAY_CONFIGS.containsKey(friendlyName)) {
- JsonConfig jsonConfig = container.get(friendlyName, null);
- if (jsonConfig != null && resolveArrayConfig(friendlyName, jsonConfig, context, classLoader)) {
- resolvedKeys.add(friendlyName);
+ JsonConfig jsonConfig = jsonConfigs.get(friendlyName);
+ if (jsonConfig != null) {
+ resolveArrayConfig(friendlyName, jsonConfig, context, classLoader);
}
}
}
@@ -135,14 +129,12 @@ public class ParseContextUtils {
try {
ComponentRegistry registry = new ComponentRegistry("other-configs", classLoader);
- for (String friendlyName : container.getKeys()) {
- // Skip already resolved array configs
- if (resolvedKeys.contains(friendlyName)) {
- continue;
- }
+ for (Map.Entry<String, JsonConfig> entry : jsonConfigs.entrySet()) {
+ String friendlyName = entry.getKey();
+ JsonConfig jsonConfig = entry.getValue();
- JsonConfig jsonConfig = container.get(friendlyName, null);
- if (jsonConfig == null) {
+ // Skip already resolved configs (including array configs)
+ if (context.getResolvedConfig(friendlyName) != null) {
continue;
}
@@ -160,10 +152,10 @@ public class ParseContextUtils {
// Determine the context key
Class<?> contextKey = determineContextKey(info, friendlyName);
- // Deserialize and add to ParseContext
+ // Deserialize and cache
Object instance = MAPPER.readValue(jsonConfig.json(), info.componentClass());
+ context.setResolvedConfig(friendlyName, instance);
context.set((Class) contextKey, instance);
- resolvedKeys.add(friendlyName);
LOG.debug("Resolved '{}' -> {} with key {}",
friendlyName, info.componentClass().getName(), contextKey.getName());
@@ -179,16 +171,6 @@ public class ParseContextUtils {
// other-configs registry not available - that's okay, array configs were still processed
LOG.debug("other-configs registry not available: {}", e.getMessage());
}
-
- // Remove resolved configs from the container
- for (String key : resolvedKeys) {
- container.remove(key);
- }
-
- // If the container is now empty, remove it from the ParseContext
- if (container.isEmpty()) {
- context.set(ConfigContainer.class, null);
- }
}
/**
@@ -289,6 +271,7 @@ public class ParseContextUtils {
if (!components.isEmpty()) {
Object composite = createComposite(configName, components, configInfo);
if (composite != null) {
+ context.setResolvedConfig(configName, composite);
context.set((Class) configInfo.contextKey(), composite);
LOG.debug("Resolved '{}' -> {} with {} components",
configName, composite.getClass().getSimpleName(), components.size());
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
new file mode 100644
index 000000000..845cdec3c
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/TikaModule.java
@@ -0,0 +1,503 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.DeserializationConfig;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.deser.Deserializers;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.ser.Serializers;
+
+import org.apache.tika.config.Initializable;
+import org.apache.tika.config.JsonConfig;
+import org.apache.tika.config.SelfConfiguring;
+import org.apache.tika.detect.DefaultDetector;
+import org.apache.tika.detect.Detector;
+import org.apache.tika.detect.EncodingDetector;
+import org.apache.tika.digest.DigesterFactory;
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractorFactory;
+import org.apache.tika.language.translate.Translator;
+import org.apache.tika.metadata.filter.MetadataFilter;
+import org.apache.tika.metadata.writefilter.MetadataWriteFilterFactory;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.DefaultParser;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.ParserDecorator;
+import org.apache.tika.renderer.Renderer;
+import org.apache.tika.serialization.serdes.DefaultDetectorDeserializer;
+import org.apache.tika.serialization.serdes.DefaultDetectorSerializer;
+import org.apache.tika.serialization.serdes.DefaultParserDeserializer;
+import org.apache.tika.serialization.serdes.DefaultParserSerializer;
+
+/**
+ * Jackson module that provides compact serialization for Tika components.
+ * <p>
+ * Uses {@link ComponentNameResolver} for friendly name resolution (e.g., "pdf-parser").
+ * <p>
+ * Supports two formats:
+ * <ol>
+ * <li>Simple string: {@code "text-parser"} β instance with defaults</li>
+ * <li>Object with type as key: {@code {"pdf-parser": {"ocrStrategy": "AUTO"}}} β instance with config</li>
+ * </ol>
+ * <p>
+ * For components implementing {@link SelfConfiguring}, uses the {@link JsonConfig} constructor.
+ * For other components, uses Jackson bean deserialization (readerForUpdating).
+ */
+public class TikaModule extends SimpleModule {
+
+ private static ObjectMapper sharedMapper;
+
+ /**
+ * Interfaces that use compact format serialization.
+ * Types implementing these interfaces will be serialized as:
+ * - "type-name" for defaults
+ * - {"type-name": {...}} for configured instances
+ */
+ private static final Set<Class<?>> COMPACT_FORMAT_INTERFACES = new HashSet<>();
+
+ static {
+ // Core component interfaces that use compact format
+ COMPACT_FORMAT_INTERFACES.add(Parser.class);
+ COMPACT_FORMAT_INTERFACES.add(Detector.class);
+ COMPACT_FORMAT_INTERFACES.add(EncodingDetector.class);
+ COMPACT_FORMAT_INTERFACES.add(MetadataFilter.class);
+ COMPACT_FORMAT_INTERFACES.add(Translator.class);
+ COMPACT_FORMAT_INTERFACES.add(Renderer.class);
+ COMPACT_FORMAT_INTERFACES.add(DigesterFactory.class);
+ COMPACT_FORMAT_INTERFACES.add(EmbeddedDocumentExtractorFactory.class);
+ COMPACT_FORMAT_INTERFACES.add(MetadataWriteFilterFactory.class);
+ }
+
+ /**
+ * Checks if a type should use compact format serialization.
+ * Returns true if the type implements any of the registered compact format interfaces.
+ */
+ private static boolean usesCompactFormat(Class<?> type) {
+ for (Class<?> iface : COMPACT_FORMAT_INTERFACES) {
+ if (iface.isAssignableFrom(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public TikaModule() {
+ super("TikaModule");
+ }
+
+ /**
+ * Sets the shared ObjectMapper for use during deserialization.
+ * Must be called before deserializing components.
+ *
+ * @param mapper the ObjectMapper with TikaModule registered
+ */
+ public static void setSharedMapper(ObjectMapper mapper) {
+ sharedMapper = mapper;
+ }
+
+ /**
+ * Gets the shared ObjectMapper.
+ *
+ * @return the shared mapper, or null if not configured
+ */
+ public static ObjectMapper getSharedMapper() {
+ return sharedMapper;
+ }
+
+ @Override
+ public void setupModule(SetupContext context) {
+ super.setupModule(context);
+ context.addDeserializers(new TikaDeserializers());
+ context.addSerializers(new TikaSerializers());
+ }
+
+ /**
+ * Deserializers for Tika component types.
+ * <p>
+ * Only handles abstract types (interfaces/abstract classes) that are compact format interfaces.
+ * Concrete implementations use normal Jackson bean deserialization for their properties.
+ */
+ private static class TikaDeserializers extends Deserializers.Base {
+ @Override
+ public JsonDeserializer<?> findBeanDeserializer(JavaType type, DeserializationConfig config,
+ BeanDescription beanDesc) {
+ Class<?> rawClass = type.getRawClass();
+
+ // Only use compact format deserializer for ABSTRACT types (interfaces or abstract classes)
+ // that are in the compact format interfaces list.
+ // Concrete implementations (like ExternalParser, HtmlParser) should use normal
+ // Jackson bean deserialization for their properties.
+ if (rawClass.isInterface() || Modifier.isAbstract(rawClass.getModifiers())) {
+ if (COMPACT_FORMAT_INTERFACES.contains(rawClass) || usesCompactFormat(rawClass)) {
+ return new TikaComponentDeserializer(rawClass);
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Serializers for Tika component types.
+ */
+ private static class TikaSerializers extends Serializers.Base {
+ @Override
+ public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type,
+ BeanDescription beanDesc) {
+ Class<?> rawClass = type.getRawClass();
+
+ // Use dedicated serializers for SPI composite types
+ if (rawClass == DefaultParser.class) {
+ return new DefaultParserSerializer();
+ }
+ if (rawClass == DefaultDetector.class) {
+ return new DefaultDetectorSerializer();
+ }
+
+ // Handle MimeFilteringDecorator specially - serialize wrapped parser with mime filters
+ if (rawClass == ParserDecorator.MimeFilteringDecorator.class) {
+ return new TikaComponentSerializer();
+ }
+
+ // Only serialize with compact format if type implements a compact format interface
+ // AND has a registered friendly name
+ if (usesCompactFormat(rawClass) && ComponentNameResolver.getFriendlyName(rawClass) != null) {
+ return new TikaComponentSerializer();
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Deserializer that handles both string and object formats for Tika components.
+ */
+ private static class TikaComponentDeserializer extends JsonDeserializer<Object> {
+ private final Class<?> expectedType;
+ // Plain mapper for property updates (avoids infinite recursion with registered types)
+ private final ObjectMapper plainMapper;
+
+ TikaComponentDeserializer(Class<?> expectedType) {
+ this.expectedType = expectedType;
+ this.plainMapper = new ObjectMapper();
+ }
+
+ @Override
+ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ JsonNode node = p.readValueAsTree();
+
+ ObjectMapper mapper = sharedMapper;
+ if (mapper == null) {
+ throw new IOException("Shared ObjectMapper not configured. " +
+ "Call TikaModule.setSharedMapper() before deserializing.");
+ }
+
+ if (node.isTextual()) {
+ // Simple string format: "pdf-parser"
+ String typeName = node.asText();
+ return instantiate(typeName, null, mapper);
+ } else if (node.isObject()) {
+ Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
+ if (!fields.hasNext()) {
+ // Empty object {} - try to create default instance if expectedType is concrete
+ try {
+ return expectedType.getDeclaredConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new IOException("Empty object for abstract type " +
+ expectedType.getSimpleName() + " - specify a concrete type name");
+ }
+ }
+ Map.Entry<String, JsonNode> entry = fields.next();
+ return instantiate(entry.getKey(), entry.getValue(), mapper);
+ } else {
+ throw new IOException("Expected string or object for " +
+ expectedType.getSimpleName() + ", got: " + node.getNodeType());
+ }
+ }
+
+ private Object instantiate(String typeName, JsonNode configNode, ObjectMapper mapper) throws IOException {
+ // Resolve the class using ComponentNameResolver
+ Class<?> clazz;
+ try {
+ clazz = ComponentNameResolver.resolveClass(typeName,
+ Thread.currentThread().getContextClassLoader());
+ } catch (ClassNotFoundException e) {
+ throw new IOException("Unknown type: " + typeName, e);
+ }
+
+ // Verify type compatibility
+ if (!expectedType.isAssignableFrom(clazz)) {
+ throw new IOException("Type " + typeName + " (" + clazz.getName() +
+ ") is not assignable to " + expectedType.getName());
+ }
+
+ // Extract mime filter fields before stripping them
+ Set<MediaType> includeTypes = extractMimeTypes(configNode, "_mime-include");
+ Set<MediaType> excludeTypes = extractMimeTypes(configNode, "_mime-exclude");
+
+ // Strip decorator fields before passing to component
+ JsonNode cleanedConfig = stripDecoratorFields(configNode);
+
+ try {
+ Object instance;
+
+ // Handle DefaultParser and DefaultDetector with dedicated deserializers
+ if (clazz == DefaultParser.class) {
+ instance = deserializeWithNode(new DefaultParserDeserializer(), cleanedConfig, mapper);
+ } else if (clazz == DefaultDetector.class) {
+ instance = deserializeWithNode(new DefaultDetectorDeserializer(), cleanedConfig, mapper);
+ } else if (cleanedConfig == null || cleanedConfig.isEmpty()) {
+ // If no config, use default constructor
+ instance = clazz.getDeclaredConstructor().newInstance();
+ } else if (SelfConfiguring.class.isAssignableFrom(clazz)) {
+ // SelfConfiguring components must use JsonConfig constructor
+ Constructor<?> jsonConfigCtor = findJsonConfigConstructor(clazz);
+ if (jsonConfigCtor != null) {
+ String json = mapper.writeValueAsString(cleanedConfig);
+ instance = jsonConfigCtor.newInstance((JsonConfig) () -> json);
+ } else {
+ throw new IOException("SelfConfiguring component '" + typeName +
+ "' must have a constructor that accepts JsonConfig, but none was found.");
+ }
+ } else {
+ // Non-SelfConfiguring: use Jackson bean deserialization
+ instance = clazz.getDeclaredConstructor().newInstance();
+ plainMapper.readerForUpdating(instance).readValue(cleanedConfig);
+ }
+
+ // Call initialize() on Initializable components
+ if (instance instanceof Initializable) {
+ try {
+ ((Initializable) instance).initialize();
+ } catch (TikaConfigException e) {
+ throw new IOException("Failed to initialize " + typeName, e);
+ }
+ }
+
+ // Wrap parser with mime filtering if include/exclude types specified
+ if (instance instanceof Parser && (!includeTypes.isEmpty() || !excludeTypes.isEmpty())) {
+ instance = ParserDecorator.withMimeFilters((Parser) instance, includeTypes, excludeTypes);
+ }
+
+ return instance;
+
+ } catch (ReflectiveOperationException e) {
+ throw new IOException("Failed to instantiate: " + typeName, e);
+ }
+ }
+
+ private Set<MediaType> extractMimeTypes(JsonNode configNode, String fieldName) {
+ Set<MediaType> types = new HashSet<>();
+ if (configNode == null || !configNode.has(fieldName)) {
+ return types;
+ }
+ JsonNode arrayNode = configNode.get(fieldName);
+ if (arrayNode.isArray()) {
+ for (JsonNode typeNode : arrayNode) {
+ types.add(MediaType.parse(typeNode.asText()));
+ }
+ }
+ return types;
+ }
+
+ private Constructor<?> findJsonConfigConstructor(Class<?> clazz) {
+ try {
+ return clazz.getConstructor(JsonConfig.class);
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Deserializes a JsonNode using a dedicated deserializer.
+ */
+ private <T> T deserializeWithNode(JsonDeserializer<T> deserializer, JsonNode node,
+ ObjectMapper mapper) throws IOException {
+ if (node == null) {
+ node = mapper.createObjectNode();
+ }
+ try (JsonParser p = mapper.treeAsTokens(node)) {
+ p.nextToken();
+ return deserializer.deserialize(p, mapper.getDeserializationContext());
+ }
+ }
+
+ /**
+ * Strips decorator fields (_mime-include, _mime-exclude) from config node.
+ * These fields are handled by TikaLoader for wrapping, not by the component itself.
+ * Note: _exclude is NOT stripped as it's used by DefaultParser for SPI exclusions.
+ */
+ private JsonNode stripDecoratorFields(JsonNode configNode) {
+ if (configNode == null || !configNode.isObject()) {
+ return configNode;
+ }
+ ObjectNode cleaned = configNode.deepCopy();
+ cleaned.remove("_mime-include");
+ cleaned.remove("_mime-exclude");
+ return cleaned;
+ }
+ }
+
+ /**
+ * Serializer that produces compact output for Tika components.
+ * Outputs simple string if using defaults, object with type key if configured.
+ */
+ private static class TikaComponentSerializer extends JsonSerializer<Object> {
+ // Plain mapper for serializing without TikaModule (avoids infinite recursion)
+ private final ObjectMapper plainMapper;
+
+ TikaComponentSerializer() {
+ this.plainMapper = new ObjectMapper();
+ this.plainMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+ }
+
+ @Override
+ public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
+ throws IOException {
+ // Handle MimeFilteringDecorator specially for round-trip
+ Set<MediaType> includeTypes = null;
+ Set<MediaType> excludeTypes = null;
+ if (value instanceof ParserDecorator.MimeFilteringDecorator mfd) {
+ includeTypes = mfd.getIncludeTypes();
+ excludeTypes = mfd.getExcludeTypes();
+ value = mfd.getWrappedParser();
+ }
+
+ String typeName = ComponentNameResolver.getFriendlyName(value.getClass());
+ if (typeName == null) {
+ typeName = value.getClass().getName();
+ }
+
+ ObjectMapper mapper = (ObjectMapper) gen.getCodec();
+
+ // Get configured properties (only non-default values)
+ ObjectNode configNode = getConfiguredProperties(value, mapper);
+
+ // Add mime filter fields if present
+ if (includeTypes != null && !includeTypes.isEmpty()) {
+ configNode.set("_mime-include", mimeTypesToArray(includeTypes, mapper));
+ }
+ if (excludeTypes != null && !excludeTypes.isEmpty()) {
+ configNode.set("_mime-exclude", mimeTypesToArray(excludeTypes, mapper));
+ }
+
+ if (configNode.isEmpty()) {
+ // No config differences - output simple string
+ gen.writeString(typeName);
+ } else {
+ // Has config - output object with type as key
+ gen.writeStartObject();
+ gen.writeObjectField(typeName, configNode);
+ gen.writeEndObject();
+ }
+ }
+
+ private JsonNode mimeTypesToArray(Set<MediaType> types, ObjectMapper mapper) {
+ var arrayNode = mapper.createArrayNode();
+ for (MediaType type : types) {
+ arrayNode.add(type.toString());
+ }
+ return arrayNode;
+ }
+
+ private ObjectNode getConfiguredProperties(Object value, ObjectMapper mapper) throws IOException {
+ try {
+ // Check for getConfig() method (common pattern for config objects)
+ Method getConfigMethod = findGetConfigMethod(value.getClass());
+
+ if (getConfigMethod != null) {
+ // Serialize the config object's properties
+ Object config = getConfigMethod.invoke(value);
+ if (config == null) {
+ return mapper.createObjectNode();
+ }
+
+ // Create default config to compare against
+ Object defaultConfig = config.getClass().getDeclaredConstructor().newInstance();
+
+ ObjectNode configNode = plainMapper.valueToTree(config);
+ ObjectNode defaultNode = plainMapper.valueToTree(defaultConfig);
+
+ // Only keep properties that differ from defaults
+ ObjectNode result = mapper.createObjectNode();
+ Iterator<Map.Entry<String, JsonNode>> fields = configNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry<String, JsonNode> field = fields.next();
+ JsonNode defaultValue = defaultNode.get(field.getKey());
+ if (!field.getValue().equals(defaultValue)) {
+ result.set(field.getKey(), field.getValue());
+ }
+ }
+ return result;
+ } else {
+ // No config object - serialize the component directly
+ Object defaultInstance = value.getClass().getDeclaredConstructor().newInstance();
+
+ ObjectNode valueNode = plainMapper.valueToTree(value);
+ ObjectNode defaultNode = plainMapper.valueToTree(defaultInstance);
+
+ ObjectNode result = plainMapper.createObjectNode();
+ Iterator<Map.Entry<String, JsonNode>> fields = valueNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry<String, JsonNode> field = fields.next();
+ JsonNode defaultValue = defaultNode.get(field.getKey());
+ if (!field.getValue().equals(defaultValue)) {
+ result.set(field.getKey(), field.getValue());
+ }
+ }
+ return result;
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IOException("Failed to serialize config", e);
+ }
+ }
+
+ private Method findGetConfigMethod(Class<?> clazz) {
+ try {
+ Method method = clazz.getMethod("getConfig");
+ if (method.getReturnType() != void.class) {
+ return method;
+ }
+ } catch (NoSuchMethodException e) {
+ // No getConfig method
+ }
+ return null;
+ }
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorDeserializer.java
new file mode 100644
index 000000000..97ce76029
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorDeserializer.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import java.util.Collection;
+
+import org.apache.tika.config.ServiceLoader;
+import org.apache.tika.detect.DefaultDetector;
+import org.apache.tika.detect.Detector;
+import org.apache.tika.mime.MimeTypes;
+
+/**
+ * Deserializer for DefaultDetector that handles exclusions.
+ * <p>
+ * Supports JSON formats:
+ * <pre>
+ * "default-detector"
+ * { "default-detector": {} }
+ * { "default-detector": { "exclude": ["html-detector", "zip-detector"] } }
+ * </pre>
+ */
+public class DefaultDetectorDeserializer extends SpiCompositeDeserializer<DefaultDetector> {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected DefaultDetector createInstance(Collection<Class<?>> excludedClasses) {
+ if (excludedClasses == null || excludedClasses.isEmpty()) {
+ return new DefaultDetector();
+ }
+ return new DefaultDetector(MimeTypes.getDefaultMimeTypes(),
+ new ServiceLoader(),
+ (Collection<Class<? extends Detector>>) (Collection<?>) excludedClasses);
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorSerializer.java
new file mode 100644
index 000000000..6dfcf0fdb
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultDetectorSerializer.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.tika.detect.DefaultDetector;
+import org.apache.tika.detect.Detector;
+
+/**
+ * Serializer for DefaultDetector that outputs exclusions.
+ * <p>
+ * Outputs:
+ * <pre>
+ * "default-detector" // if no exclusions
+ * { "default-detector": { "exclude": ["html-detector"] } } // with exclusions
+ * </pre>
+ */
+public class DefaultDetectorSerializer extends SpiCompositeSerializer<DefaultDetector> {
+
+ public DefaultDetectorSerializer() {
+ super("default-detector");
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected Set<Class<?>> getExcludedClasses(DefaultDetector value) {
+ Collection<Class<? extends Detector>> excluded = value.getExcludedClasses();
+ if (excluded == null || excluded.isEmpty()) {
+ return null;
+ }
+ return new HashSet<>((Collection<Class<?>>) (Collection<?>) excluded);
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserDeserializer.java
new file mode 100644
index 000000000..ffc491e61
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserDeserializer.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import java.util.Collection;
+
+import org.apache.tika.config.ServiceLoader;
+import org.apache.tika.mime.MediaTypeRegistry;
+import org.apache.tika.parser.DefaultParser;
+import org.apache.tika.parser.Parser;
+
+/**
+ * Deserializer for DefaultParser that handles exclusions.
+ * <p>
+ * Supports JSON formats:
+ * <pre>
+ * "default-parser"
+ * { "default-parser": {} }
+ * { "default-parser": { "exclude": ["html-parser", "xml-parser"] } }
+ * </pre>
+ */
+public class DefaultParserDeserializer extends SpiCompositeDeserializer<DefaultParser> {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected DefaultParser createInstance(Collection<Class<?>> excludedClasses) {
+ if (excludedClasses == null || excludedClasses.isEmpty()) {
+ return new DefaultParser();
+ }
+ return new DefaultParser(MediaTypeRegistry.getDefaultRegistry(),
+ new ServiceLoader(),
+ (Collection<Class<? extends Parser>>) (Collection<?>) excludedClasses);
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserSerializer.java
new file mode 100644
index 000000000..a32f83f04
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/DefaultParserSerializer.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.tika.parser.DefaultParser;
+import org.apache.tika.parser.Parser;
+
+/**
+ * Serializer for DefaultParser that outputs exclusions.
+ * <p>
+ * Outputs:
+ * <pre>
+ * "default-parser" // if no exclusions
+ * { "default-parser": { "exclude": ["html-parser"] } } // with exclusions
+ * </pre>
+ */
+public class DefaultParserSerializer extends SpiCompositeSerializer<DefaultParser> {
+
+ public DefaultParserSerializer() {
+ super("default-parser");
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected Set<Class<?>> getExcludedClasses(DefaultParser value) {
+ Collection<Class<? extends Parser>> excluded = value.getExcludedClasses();
+ if (excluded == null || excluded.isEmpty()) {
+ return null;
+ }
+ return new HashSet<>((Collection<Class<?>>) (Collection<?>) excluded);
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/MetadataDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/MetadataDeserializer.java
similarity index 98%
rename from tika-serialization/src/main/java/org/apache/tika/serialization/MetadataDeserializer.java
rename to tika-serialization/src/main/java/org/apache/tika/serialization/serdes/MetadataDeserializer.java
index 4dc7c3a7e..f02e12f30 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/MetadataDeserializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/MetadataDeserializer.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.serialization;
+package org.apache.tika.serialization.serdes;
import java.io.IOException;
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/MetadataSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/MetadataSerializer.java
similarity index 98%
rename from tika-serialization/src/main/java/org/apache/tika/serialization/MetadataSerializer.java
rename to tika-serialization/src/main/java/org/apache/tika/serialization/serdes/MetadataSerializer.java
index 68e7d6593..0dd060715 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/MetadataSerializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/MetadataSerializer.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.serialization;
+package org.apache.tika.serialization.serdes;
import java.io.IOException;
import java.util.Arrays;
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
new file mode 100644
index 000000000..2e566f4fa
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextDeserializer.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import static org.apache.tika.serialization.serdes.ParseContextSerializer.PARSE_CONTEXT;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.serialization.ConfigDeserializer;
+
+/**
+ * Deserializes ParseContext from JSON.
+ * <p>
+ * Each field in the JSON object is stored as a JSON config in the ParseContext.
+ * Resolution to typed objects happens later via {@link ConfigDeserializer}.
+ * <p>
+ * Example input:
+ * <pre>
+ * {
+ * "pdf-parser": {"ocrStrategy": "AUTO"},
+ * "handler-config": {"type": "XML", "parseMode": "RMETA"}
+ * }
+ * </pre>
+ */
+public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
+
+ @Override
+ public ParseContext deserialize(JsonParser jsonParser, DeserializationContext ctxt)
+ throws IOException {
+ JsonNode root = jsonParser.readValueAsTree();
+ return readParseContext(root, (ObjectMapper) jsonParser.getCodec());
+ }
+
+ /**
+ * Deserializes a ParseContext from a JsonNode.
+ * <p>
+ * Each field is stored as a JSON config string in the ParseContext's jsonConfigs map.
+ * The configs can later be resolved to typed objects via {@link ConfigDeserializer}.
+ *
+ * @param jsonNode the JSON node containing the ParseContext data
+ * @param mapper the ObjectMapper for serializing field values back to JSON strings
+ * @return the deserialized ParseContext with jsonConfigs populated
+ * @throws IOException if deserialization fails
+ */
+ public static ParseContext readParseContext(JsonNode jsonNode, ObjectMapper mapper)
+ throws IOException {
+ // Handle optional wrapper: { "parseContext": {...} }
+ JsonNode contextNode = jsonNode.get(PARSE_CONTEXT);
+ if (contextNode == null) {
+ contextNode = jsonNode;
+ }
+
+ ParseContext parseContext = new ParseContext();
+
+ if (!contextNode.isObject()) {
+ return parseContext;
+ }
+
+ // Store each field as a JSON config
+ Iterator<String> fieldNames = contextNode.fieldNames();
+ while (fieldNames.hasNext()) {
+ String name = fieldNames.next();
+ JsonNode value = contextNode.get(name);
+ // Store the JSON string for later resolution
+ String json = mapper.writeValueAsString(value);
+ parseContext.setJsonConfig(name, json);
+ }
+
+ return parseContext;
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
new file mode 100644
index 000000000..f0648e5f8
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/ParseContextSerializer.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import java.io.IOException;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import org.apache.tika.config.JsonConfig;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Serializes ParseContext to JSON.
+ * <p>
+ * Serializes the jsonConfigs map directly - each entry becomes a field in the output.
+ * The resolvedConfigs cache is intentionally ignored to preserve round-trip fidelity.
+ * <p>
+ * Example output:
+ * <pre>
+ * {
+ * "pdf-parser": {"ocrStrategy": "AUTO"},
+ * "handler-config": {"type": "XML", "parseMode": "RMETA"}
+ * }
+ * </pre>
+ */
+public class ParseContextSerializer extends JsonSerializer<ParseContext> {
+
+ public static final String PARSE_CONTEXT = "parseContext";
+
+ @Override
+ public void serialize(ParseContext parseContext, JsonGenerator gen,
+ SerializerProvider serializers) throws IOException {
+ gen.writeStartObject();
+
+ // Serialize all JSON configs - this is the source of truth for round-trip
+ Map<String, JsonConfig> jsonConfigs = parseContext.getJsonConfigs();
+ for (Map.Entry<String, JsonConfig> entry : jsonConfigs.entrySet()) {
+ gen.writeFieldName(entry.getKey());
+ // Write the JSON config as raw value (it's already valid JSON)
+ gen.writeRawValue(entry.getValue().json());
+ }
+
+ gen.writeEndObject();
+ }
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeDeserializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeDeserializer.java
new file mode 100644
index 000000000..0218ca3d8
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeDeserializer.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import org.apache.tika.serialization.ComponentNameResolver;
+
+/**
+ * Abstract base deserializer for SPI-loaded composite types that support exclusions.
+ * <p>
+ * Handles JSON like:
+ * <pre>
+ * { "exclude": ["html-detector", "zip-detector"] }
+ * </pre>
+ * or simply an empty object:
+ * <pre>
+ * {}
+ * </pre>
+ * <p>
+ * Note: The outer type wrapper (e.g., "default-detector") is handled by TikaModule.
+ * This deserializer receives just the inner config object.
+ * <p>
+ * Subclasses implement {@link #createInstance(Collection)} to create the appropriate
+ * composite type with the exclusions applied.
+ *
+ * @param <T> the composite type (e.g., DefaultDetector, DefaultParser)
+ */
+public abstract class SpiCompositeDeserializer<T> extends JsonDeserializer<T> {
+
+ @Override
+ public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ JsonNode node = p.readValueAsTree();
+ Collection<Class<?>> excludedClasses = parseExclusions(node);
+ return createInstance(excludedClasses);
+ }
+
+ /**
+ * Parse exclusions from config node.
+ * Supports both "exclude" and "_exclude" field names.
+ */
+ protected Collection<Class<?>> parseExclusions(JsonNode node) throws IOException {
+ Set<Class<?>> excludedClasses = new HashSet<>();
+
+ if (node == null || !node.isObject()) {
+ return excludedClasses;
+ }
+
+ // Support both "exclude" and "_exclude" for compatibility
+ JsonNode excludeNode = node.has("exclude") ? node.get("exclude") : node.get("_exclude");
+
+ if (excludeNode != null && excludeNode.isArray()) {
+ for (JsonNode item : excludeNode) {
+ String typeName = item.asText();
+ try {
+ Class<?> clazz = ComponentNameResolver.resolveClass(typeName,
+ Thread.currentThread().getContextClassLoader());
+ excludedClasses.add(clazz);
+ } catch (ClassNotFoundException e) {
+ throw new IOException("Unknown type in exclude list: " + typeName, e);
+ }
+ }
+ }
+
+ return excludedClasses;
+ }
+
+ /**
+ * Create an instance of the composite type with the specified exclusions.
+ *
+ * @param excludedClasses classes to exclude from SPI loading
+ * @return the new instance
+ */
+ protected abstract T createInstance(Collection<Class<?>> excludedClasses);
+}
diff --git a/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeSerializer.java b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeSerializer.java
new file mode 100644
index 000000000..b5f6b8428
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/serdes/SpiCompositeSerializer.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization.serdes;
+
+import java.io.IOException;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import org.apache.tika.serialization.ComponentNameResolver;
+
+/**
+ * Abstract base serializer for SPI-loaded composite types that support exclusions.
+ * <p>
+ * Outputs JSON like:
+ * <pre>
+ * "default-detector" // if no exclusions
+ * { "default-detector": { "exclude": ["html-detector", "zip-detector"] } } // with exclusions
+ * </pre>
+ *
+ * @param <T> the composite type (e.g., DefaultDetector, DefaultParser)
+ */
+public abstract class SpiCompositeSerializer<T> extends JsonSerializer<T> {
+
+ protected final String typeName;
+
+ protected SpiCompositeSerializer(String typeName) {
+ this.typeName = typeName;
+ }
+
+ @Override
+ public void serialize(T value, JsonGenerator gen, SerializerProvider serializers)
+ throws IOException {
+ Set<Class<?>> excludedClasses = getExcludedClasses(value);
+
+ if (excludedClasses == null || excludedClasses.isEmpty()) {
+ gen.writeString(typeName);
+ } else {
+ gen.writeStartObject();
+ gen.writeObjectFieldStart(typeName);
+ gen.writeArrayFieldStart("exclude");
+ for (Class<?> clazz : excludedClasses) {
+ String name = ComponentNameResolver.getFriendlyName(clazz);
+ if (name != null) {
+ gen.writeString(name);
+ } else {
+ // Fall back to fully qualified class name
+ gen.writeString(clazz.getName());
+ }
+ }
+ gen.writeEndArray();
+ gen.writeEndObject();
+ gen.writeEndObject();
+ }
+ }
+
+ /**
+ * Get the excluded classes from the composite instance.
+ *
+ * @param value the composite instance
+ * @return the set of excluded classes, or null/empty if none
+ */
+ protected abstract Set<Class<?>> getExcludedClasses(T value);
+}
diff --git a/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaLoaderRoundTripTest.java b/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaLoaderRoundTripTest.java
new file mode 100644
index 000000000..036d242e8
--- /dev/null
+++ b/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaLoaderRoundTripTest.java
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.config.loader;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.CompositeParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+
+/**
+ * Tests for TikaLoader round-trip serialization (load -> toJson -> reload).
+ */
+public class TikaLoaderRoundTripTest {
+
+ @Test
+ void testBasicRoundTrip() throws Exception {
+ URL configUrl = getClass().getResource("/configs/test-default-parser-with-exclusions.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ // Load config
+ TikaLoader loader = TikaLoader.load(configPath);
+
+ // Force component loading
+ Parser parser = loader.get(Parser.class);
+ assertNotNull(parser);
+
+ // Serialize
+ String json = loader.toJson();
+ assertNotNull(json);
+ assertTrue(json.contains("parsers"));
+
+ // Reload from serialized JSON
+ Path tempFile = Files.createTempFile("tika-roundtrip-", ".json");
+ try {
+ Files.writeString(tempFile, json);
+ TikaLoader reloaded = TikaLoader.load(tempFile);
+
+ Parser reloadedParser = reloaded.get(Parser.class);
+ assertNotNull(reloadedParser);
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ @Test
+ void testExclusionsPreservedInRoundTrip() throws Exception {
+ URL configUrl = getClass().getResource("/configs/test-default-parser-with-exclusions.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ // Load config with exclusions
+ TikaLoader loader = TikaLoader.load(configPath);
+ CompositeParser parser = (CompositeParser) loader.get(Parser.class);
+
+ // Verify exclusions are applied
+ ParseContext context = new ParseContext();
+ assertFalse(parser.getSupportedTypes(context).contains(MediaType.parse("application/test+minimal")),
+ "minimal-test-parser should be excluded");
+
+ // Round-trip
+ String json = loader.toJson();
+ Path tempFile = Files.createTempFile("tika-exclusions-", ".json");
+ try {
+ Files.writeString(tempFile, json);
+ TikaLoader reloaded = TikaLoader.load(tempFile);
+
+ CompositeParser reloadedParser = (CompositeParser) reloaded.get(Parser.class);
+
+ // Exclusions should still be applied
+ assertFalse(reloadedParser.getSupportedTypes(context)
+ .contains(MediaType.parse("application/test+minimal")),
+ "Exclusions should survive round-trip");
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ @Test
+ void testMultipleRoundTripsStability() throws Exception {
+ URL configUrl = getClass().getResource("/configs/test-default-parser-with-exclusions.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ TikaLoader loader = TikaLoader.load(configPath);
+ loader.get(Parser.class); // Force loading
+
+ // Perform 3 round-trips
+ for (int i = 0; i < 3; i++) {
+ String json = loader.toJson();
+ Path tempFile = Files.createTempFile("tika-multi-", ".json");
+ try {
+ Files.writeString(tempFile, json);
+ loader = TikaLoader.load(tempFile);
+ loader.get(Parser.class); // Force loading
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ // Should still work
+ Parser parser = loader.get(Parser.class);
+ assertNotNull(parser);
+ }
+
+ @Test
+ void testUnloadedComponentsPreserved() throws Exception {
+ URL configUrl = getClass().getResource("/configs/test-default-parser-with-exclusions.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ // Load but DON'T access Parser (lazy loading)
+ TikaLoader loader = TikaLoader.load(configPath);
+
+ // Serialize without loading Parser
+ String json = loader.toJson();
+
+ // Should still contain parsers section from original JSON
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode node = mapper.readTree(json);
+ assertTrue(node.has("parsers"), "Unloaded component should be preserved");
+ }
+
+ @Test
+ void testToJsonFormat() throws Exception {
+ URL configUrl = getClass().getResource("/configs/test-default-parser-with-exclusions.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ TikaLoader loader = TikaLoader.load(configPath);
+ loader.get(Parser.class);
+
+ String json = loader.toJson();
+
+ // Should be pretty-printed
+ assertTrue(json.contains("\n"), "Should be pretty-printed");
+
+ // Should be valid JSON
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode node = mapper.readTree(json);
+ assertNotNull(node);
+ }
+
+ @Test
+ void testSaveToFile() throws Exception {
+ URL configUrl = getClass().getResource("/configs/test-default-parser-with-exclusions.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ TikaLoader loader = TikaLoader.load(configPath);
+ loader.get(Parser.class);
+
+ // Save to file
+ Path tempFile = Files.createTempFile("tika-save-", ".json");
+ try {
+ loader.save(tempFile.toFile());
+
+ // Verify file was written and is valid JSON
+ String content = Files.readString(tempFile);
+ assertTrue(content.contains("parsers"));
+
+ // Should be loadable
+ TikaLoader reloaded = TikaLoader.load(tempFile);
+ assertNotNull(reloaded.get(Parser.class));
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ @Test
+ void testDefaultLoaderToJson() throws Exception {
+ // Default loader with no config file
+ TikaLoader loader = TikaLoader.loadDefault();
+ loader.get(Parser.class);
+
+ String json = loader.toJson();
+ assertNotNull(json);
+
+ // Should produce valid JSON with parsers
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode node = mapper.readTree(json);
+ assertTrue(node.has("parsers"), "Default loader should serialize parsers");
+ }
+
+ @Test
+ void testExcludeFieldInOutput() throws Exception {
+ URL configUrl = getClass().getResource("/configs/test-default-parser-with-exclusions.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ TikaLoader loader = TikaLoader.load(configPath);
+ loader.get(Parser.class);
+
+ String json = loader.toJson();
+
+ // Should contain exclude in output (for default-parser exclusions)
+ // The config has _exclude, serializer outputs exclude
+ assertTrue(json.contains("exclude") || json.contains("_exclude"),
+ "Exclusions should be in output");
+ }
+
+ @Test
+ void testMimeFilteringRoundTrip() throws Exception {
+ // Test that _mime-include/_mime-exclude survives round-trip
+ URL configUrl = getClass().getResource("/configs/test-decoration-config.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ // Load config with mime filtering
+ TikaLoader loader = TikaLoader.load(configPath);
+ Parser parser = loader.get(Parser.class);
+ ParseContext context = new ParseContext();
+
+ // Verify initial filtering works
+ assertTrue(parser.getSupportedTypes(context).contains(MediaType.parse("application/pdf")),
+ "Should support application/pdf");
+ assertTrue(parser.getSupportedTypes(context).contains(MediaType.parse("text/plain")),
+ "Should support text/plain");
+ assertFalse(parser.getSupportedTypes(context).contains(MediaType.parse("application/pdf+fdf")),
+ "Should NOT support application/pdf+fdf (excluded)");
+
+ // Round-trip
+ String json = loader.toJson();
+
+ // Verify JSON contains the mime filter fields
+ assertTrue(json.contains("_mime-include") || json.contains("mime-include"),
+ "JSON should contain mime-include");
+
+ // Reload and verify filtering still works
+ Path tempFile = Files.createTempFile("tika-mime-filter-", ".json");
+ try {
+ Files.writeString(tempFile, json);
+ TikaLoader reloaded = TikaLoader.load(tempFile);
+ Parser reloadedParser = reloaded.get(Parser.class);
+
+ assertTrue(reloadedParser.getSupportedTypes(context).contains(MediaType.parse("application/pdf")),
+ "After round-trip: Should support application/pdf");
+ assertTrue(reloadedParser.getSupportedTypes(context).contains(MediaType.parse("text/plain")),
+ "After round-trip: Should support text/plain");
+ assertFalse(reloadedParser.getSupportedTypes(context).contains(MediaType.parse("application/pdf+fdf")),
+ "After round-trip: Should NOT support application/pdf+fdf");
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ @Test
+ void testMimeFilteringMultipleRoundTrips() throws Exception {
+ // Test stability across multiple round-trips
+ URL configUrl = getClass().getResource("/configs/test-decoration-config.json");
+ Path configPath = Path.of(configUrl.toURI());
+
+ TikaLoader loader = TikaLoader.load(configPath);
+ ParseContext context = new ParseContext();
+
+ // Perform 3 round-trips
+ for (int i = 0; i < 3; i++) {
+ Parser parser = loader.get(Parser.class);
+
+ // Verify filtering works after each round-trip
+ assertTrue(parser.getSupportedTypes(context).contains(MediaType.parse("application/pdf")),
+ "Round " + i + ": Should support application/pdf");
+ assertFalse(parser.getSupportedTypes(context).contains(MediaType.parse("application/pdf+fdf")),
+ "Round " + i + ": Should NOT support application/pdf+fdf");
+
+ String json = loader.toJson();
+ Path tempFile = Files.createTempFile("tika-mime-multi-", ".json");
+ try {
+ Files.writeString(tempFile, json);
+ loader = TikaLoader.load(tempFile);
+ } finally {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+ }
+}
diff --git a/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaLoaderTest.java b/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaLoaderTest.java
index cbe83c935..6ae0acd3d 100644
--- a/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaLoaderTest.java
+++ b/tika-serialization/src/test/java/org/apache/tika/config/loader/TikaLoaderTest.java
@@ -17,6 +17,7 @@
package org.apache.tika.config.loader;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -25,12 +26,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.io.TikaInputStream;
-import org.apache.tika.language.translate.EmptyTranslator;
-import org.apache.tika.language.translate.Translator;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.ParseContext;
@@ -49,7 +49,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser parser = loader.loadParsers();
+ Parser parser = loader.get(Parser.class);
assertNotNull(parser, "Parser should not be null");
}
@@ -59,7 +59,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
// Parse with the composite parser to verify config was applied
Metadata metadata = new Metadata();
@@ -82,7 +82,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser parser = loader.loadParsers();
+ Parser parser = loader.get(Parser.class);
ParseContext context = new ParseContext();
@@ -91,6 +91,10 @@ public class TikaLoaderTest {
"Should support application/pdf");
assertTrue(parser.getSupportedTypes(context).contains(MediaType.parse("text/plain")),
"Should support text/plain");
+
+ // Test that excluded types are not supported
+ assertFalse(parser.getSupportedTypes(context).contains(MediaType.parse("application/pdf+fdf")),
+ "Should NOT support application/pdf+fdf (excluded)");
}
@Test
@@ -104,11 +108,11 @@ public class TikaLoaderTest {
assertNotNull(loader, "Loader should be created");
// Load parsers
- Parser parser1 = loader.loadParsers();
+ Parser parser1 = loader.get(Parser.class);
assertNotNull(parser1, "First load should return parser");
// Load again - should return cached instance
- Parser parser2 = loader.loadParsers();
+ Parser parser2 = loader.get(Parser.class);
assertTrue(parser1 == parser2, "Should return same cached instance");
}
@@ -118,7 +122,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
// Parse with minimal parser type
Metadata metadata = new Metadata();
@@ -138,7 +142,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
// Parse with fallback parser type
Metadata metadata = new Metadata();
@@ -160,7 +164,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
// Parse with ConfigurableTestParser - should use the explicitly configured instance
Metadata metadata = new Metadata();
@@ -195,7 +199,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
// Verify ConfigurableTestParser uses the configured instance
Metadata configurableMetadata = new Metadata();
@@ -227,7 +231,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
ParseContext context = new ParseContext();
@@ -254,7 +258,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
ParseContext context = new ParseContext();
@@ -281,7 +285,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
// Parse with the opt-in parser
Metadata metadata = new Metadata();
@@ -303,7 +307,7 @@ public class TikaLoaderTest {
Path configPath = Path.of(configUrl.toURI());
TikaLoader loader = TikaLoader.load(configPath);
- Parser compositeParser = loader.loadParsers();
+ Parser compositeParser = loader.get(Parser.class);
ParseContext context = new ParseContext();
@@ -318,47 +322,8 @@ public class TikaLoaderTest {
"Should NOT support application/test+optin (opt-in only, not in SPI)");
}
- @Test
- public void testTranslatorLoading() throws Exception {
- URL configUrl = getClass().getResource("/configs/test-translator-config.json");
- Path configPath = Path.of(configUrl.toURI());
-
- TikaLoader loader = TikaLoader.load(configPath);
- Translator translator = loader.loadTranslator();
-
- assertNotNull(translator, "Translator should not be null");
- assertTrue(translator instanceof EmptyTranslator, "Should be EmptyTranslator");
- assertTrue(translator.isAvailable(), "Translator should be available");
- }
-
- @Test
- public void testTranslatorLazyLoading() throws Exception {
- URL configUrl = getClass().getResource("/configs/test-translator-config.json");
- Path configPath = Path.of(configUrl.toURI());
-
- TikaLoader loader = TikaLoader.load(configPath);
-
- // Load translator
- Translator translator1 = loader.loadTranslator();
- assertNotNull(translator1, "First load should return translator");
-
- // Load again - should return cached instance
- Translator translator2 = loader.loadTranslator();
- assertTrue(translator1 == translator2, "Should return same cached instance");
- }
-
- @Test
- public void testDefaultTranslatorWhenNotConfigured() throws Exception {
- URL configUrl = getClass().getResource("/configs/test-loader-config.json");
- Path configPath = Path.of(configUrl.toURI());
-
- TikaLoader loader = TikaLoader.load(configPath);
- Translator translator = loader.loadTranslator();
-
- assertNotNull(translator, "Translator should not be null");
- // Should be DefaultTranslator since no translator configured in test-loader-config.json
- }
-
+ // TODO: TIKA-SERIALIZATION-FOLLOWUP - Implement validation for common typos
+ @Disabled("TIKA-SERIALIZATION-FOLLOWUP: Validation for _excludes typo not yet implemented")
@Test
public void testExcludesInsteadOfExcludeThrowsException() throws Exception {
// Create a config with the common mistake: "_excludes" instead of "_exclude"
@@ -380,7 +345,7 @@ public class TikaLoaderTest {
// Attempt to load should throw TikaConfigException
try {
TikaLoader loader = TikaLoader.load(tempFile);
- loader.loadParsers();
+ loader.get(Parser.class);
throw new AssertionError("Expected TikaConfigException to be thrown");
} catch (org.apache.tika.exception.TikaConfigException e) {
// Expected - verify the error message is helpful
diff --git a/tika-serialization/src/test/java/org/apache/tika/digest/MockDigesterFactory.java b/tika-serialization/src/test/java/org/apache/tika/digest/MockDigesterFactory.java
new file mode 100644
index 000000000..a8cbc6930
--- /dev/null
+++ b/tika-serialization/src/test/java/org/apache/tika/digest/MockDigesterFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.digest;
+
+import org.apache.tika.config.TikaComponent;
+
+@TikaComponent
+public class MockDigesterFactory implements DigesterFactory {
+
+ @Override
+ public Digester build() {
+ return new InputStreamDigester(1000000, "SHA-256", "X-TIKA:digest:SHA-256", new MockEncoder());
+ }
+
+ private static class MockEncoder implements Encoder {
+
+ @Override
+ public String encode(byte[] bytes) {
+ StringBuilder hexString = new StringBuilder(2 * bytes.length);
+ for (int i = 0; i < bytes.length; i++) {
+ String hex = Integer.toHexString(0xff & bytes[i]);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ }
+ }
+
+}
diff --git a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/MetadataListFilterTest.java b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/MetadataListFilterTest.java
index ba2c481ff..22120707e 100644
--- a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/MetadataListFilterTest.java
+++ b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/MetadataListFilterTest.java
@@ -29,7 +29,7 @@ public class MetadataListFilterTest extends TikaTest {
@Test
public void testBasic() throws Exception {
TikaLoader loader = TikaLoader.load(getConfigPath(getClass(), "metadatafilter-config.json"));
- CompositeMetadataFilter compositeMetadataListFilter = (CompositeMetadataFilter) loader.loadMetadataFilters();
+ CompositeMetadataFilter compositeMetadataListFilter = (CompositeMetadataFilter) loader.get(MetadataFilter.class);
assertEquals(1, compositeMetadataListFilter.getFilters().size());
assertTrue(compositeMetadataListFilter.getFilters().get(0) instanceof AttachmentCountingListFilter);
}
diff --git a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java
index f8e750bab..0c24b6860 100644
--- a/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java
+++ b/tika-serialization/src/test/java/org/apache/tika/metadata/filter/TestMetadataFilter.java
@@ -88,7 +88,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set("author", "author");
metadata.set("content", "content");
- metadata = filterOne(loader.loadMetadataFilters(), metadata);
+ metadata = filterOne(loader.get(MetadataFilter.class), metadata);
assertEquals(2, metadata.size());
assertEquals("title", metadata.get("title"));
@@ -103,7 +103,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set("author", "author");
metadata.set("content", "content");
- metadata = filterOne(loader.loadMetadataFilters(), metadata);
+ metadata = filterOne(loader.get(MetadataFilter.class), metadata);
assertEquals(1, metadata.size());
assertEquals("content", metadata.get("content"));
}
@@ -119,7 +119,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set("author", "author");
metadata.set("content", "content");
- metadata = filterOne(loader.loadMetadataFilters(), metadata);
+ metadata = filterOne(loader.get(MetadataFilter.class), metadata);
assertEquals(2, metadata.size());
assertArrayEquals(expectedTitles, metadata.getValues("title"));
@@ -152,7 +152,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set(Metadata.CONTENT_TYPE, MediaType.image("jpeg").toString());
metadata.set("author", "author");
- MetadataFilter filter = loader.loadMetadataFilters();
+ MetadataFilter filter = loader.get(MetadataFilter.class);
metadata = filterOne(filter, metadata);
assertEquals(0, metadata.size());
@@ -172,7 +172,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set("author", "author");
metadata.set("a", "a-value");
- MetadataFilter filter = loader.loadMetadataFilters();
+ MetadataFilter filter = loader.get(MetadataFilter.class);
metadata = filterOne(filter, metadata);
assertEquals("quick brown fox", metadata.get("content"));
assertEquals("a-value", metadata.get("b"));
@@ -200,7 +200,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set(TikaCoreProperties.TIKA_CONTENT, "quick brown fox");
metadata.set(Metadata.CONTENT_TYPE, "text/html; charset=UTF-8");
- MetadataFilter filter = loader.loadMetadataFilters();
+ MetadataFilter filter = loader.get(MetadataFilter.class);
metadata = filterOne(filter, metadata);
assertEquals("quick brown fox", metadata.get(TikaCoreProperties.TIKA_CONTENT));
assertEquals("text/html", metadata.get("mime"));
@@ -214,7 +214,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set(TikaCoreProperties.TIKA_CONTENT, "quick brown fox");
metadata.set(Metadata.CONTENT_TYPE, "text/html");
- MetadataFilter filter = loader.loadMetadataFilters();
+ MetadataFilter filter = loader.get(MetadataFilter.class);
metadata = filterOne(filter, metadata);
assertEquals("quick brown fox", metadata.get(TikaCoreProperties.TIKA_CONTENT));
assertEquals("text/html", metadata.get("mime"));
@@ -228,7 +228,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set(TikaCoreProperties.TIKA_CONTENT, "quick brown fox");
metadata.set(Metadata.CONTENT_TYPE, "text/html; charset=UTF-8");
- MetadataFilter filter = loader.loadMetadataFilters();
+ MetadataFilter filter = loader.get(MetadataFilter.class);
metadata = filterOne(filter, metadata);
assertEquals("quick brown fox", metadata.get(TikaCoreProperties.TIKA_CONTENT));
assertEquals("text/html", metadata.get(Metadata.CONTENT_TYPE));
@@ -251,7 +251,7 @@ public class TestMetadataFilter extends TikaTest {
metadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, TikaCoreProperties.EmbeddedResourceType.INLINE.name());
metadata.set(Metadata.CONTENT_TYPE, "text/html; charset=UTF-8");
- MetadataFilter filter = loader.loadMetadataFilters();
+ MetadataFilter filter = loader.get(MetadataFilter.class);
metadata = filterOne(filter, metadata);
assertEquals(0, metadata.names().length);
diff --git a/tika-serialization/src/test/java/org/apache/tika/metadata/writefilter/StandardWriteFilterTest.java b/tika-serialization/src/test/java/org/apache/tika/metadata/writefilter/StandardWriteFilterTest.java
index 21a8f2551..2c7615e20 100644
--- a/tika-serialization/src/test/java/org/apache/tika/metadata/writefilter/StandardWriteFilterTest.java
+++ b/tika-serialization/src/test/java/org/apache/tika/metadata/writefilter/StandardWriteFilterTest.java
@@ -52,7 +52,7 @@ public class StandardWriteFilterTest extends TikaTest {
AutoDetectParser parser = (AutoDetectParser) loader.loadAutoDetectParser();
AutoDetectParserConfig config = parser.getAutoDetectParserConfig();
MetadataWriteFilterFactory factory = config.getMetadataWriteFilterFactory();
- assertEquals(360, ((StandardWriteFilterFactory) factory).getMaxTotalEstimatedBytes());
+ assertEquals(150, ((StandardWriteFilterFactory) factory).getMaxTotalEstimatedBytes());
String mock = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" +
"<mock>";
for (int i = 0; i < 20; i++) {
@@ -68,8 +68,8 @@ public class StandardWriteFilterTest extends TikaTest {
metadata = metadataList.get(0);
String[] creators = metadata.getValues("dc:creator");
- assertEquals(3, creators.length);
- assertEquals("01234", creators[2]);
+ assertEquals(2, creators.length);
+ assertEquals("012345678901", creators[1]);
assertContainsCount(" hello ", metadata.get(TikaCoreProperties.TIKA_CONTENT), 30);
assertTruncated(metadata);
}
diff --git a/tika-serialization/src/test/java/org/apache/tika/parser/external2/ExternalParserTest.java b/tika-serialization/src/test/java/org/apache/tika/parser/external2/ExternalParserTest.java
index 6e70feb95..35e2a7a89 100644
--- a/tika-serialization/src/test/java/org/apache/tika/parser/external2/ExternalParserTest.java
+++ b/tika-serialization/src/test/java/org/apache/tika/parser/external2/ExternalParserTest.java
@@ -44,7 +44,7 @@ public class ExternalParserTest extends TikaTest {
"file", "--version"
}));
TikaLoader loader = TikaLoader.load(getConfigPath(getClass(), "TIKA-3557.json"));
- CompositeParser p = (CompositeParser) loader.loadParsers();
+ CompositeParser p = (CompositeParser) loader.get(Parser.class);
assertEquals(1, p.getAllComponentParsers().size());
Parser parser = p.getAllComponentParsers().get(0);
// When _mime-include is used, the parser is wrapped in a ParserDecorator
@@ -71,7 +71,7 @@ public class ExternalParserTest extends TikaTest {
public void testConfigBasic() throws Exception {
assumeTrue(org.apache.tika.parser.external.ExternalParser.check(new String[]{"file", "--version"}));
TikaLoader loader = TikaLoader.load(getConfigPath(getClass(), "TIKA-3557-no-output-parser.json"));
- CompositeParser p = (CompositeParser) loader.loadParsers();
+ CompositeParser p = (CompositeParser) loader.get(Parser.class);
assertEquals(1, p.getAllComponentParsers().size());
Parser parser = p.getAllComponentParsers().get(0);
// When _mime-include is used, the parser is wrapped in a ParserDecorator
diff --git a/tika-serialization/src/test/java/org/apache/tika/sax/UppercasingContentHandler.java b/tika-serialization/src/test/java/org/apache/tika/sax/UppercasingContentHandler.java
new file mode 100644
index 000000000..f2c9cbe77
--- /dev/null
+++ b/tika-serialization/src/test/java/org/apache/tika/sax/UppercasingContentHandler.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.sax;
+
+import java.util.Locale;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+
+/**
+ * A ContentHandler decorator that converts all character content to uppercase.
+ * Used for testing custom ContentHandlerFactory configurations.
+ */
+public class UppercasingContentHandler implements ContentHandler {
+
+ private final ContentHandler delegate;
+
+ public UppercasingContentHandler(ContentHandler delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void setDocumentLocator(Locator locator) {
+ delegate.setDocumentLocator(locator);
+ }
+
+ @Override
+ public void startDocument() throws SAXException {
+ delegate.startDocument();
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ delegate.endDocument();
+ }
+
+ @Override
+ public void startPrefixMapping(String prefix, String uri) throws SAXException {
+ delegate.startPrefixMapping(prefix, uri);
+ }
+
+ @Override
+ public void endPrefixMapping(String prefix) throws SAXException {
+ delegate.endPrefixMapping(prefix);
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes atts)
+ throws SAXException {
+ delegate.startElement(uri, localName, qName, atts);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ delegate.endElement(uri, localName, qName);
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ // Convert characters to uppercase
+ char[] upper = new String(ch, start, length).toUpperCase(Locale.ROOT).toCharArray();
+ delegate.characters(upper, 0, upper.length);
+ }
+
+ @Override
+ public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
+ delegate.ignorableWhitespace(ch, start, length);
+ }
+
+ @Override
+ public void processingInstruction(String target, String data) throws SAXException {
+ delegate.processingInstruction(target, data);
+ }
+
+ @Override
+ public void skippedEntity(String name) throws SAXException {
+ delegate.skippedEntity(name);
+ }
+
+ /**
+ * Returns the underlying delegate handler's string representation,
+ * which typically contains the extracted content.
+ */
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+}
diff --git a/tika-serialization/src/test/java/org/apache/tika/sax/UppercasingContentHandlerFactory.java b/tika-serialization/src/test/java/org/apache/tika/sax/UppercasingContentHandlerFactory.java
new file mode 100644
index 000000000..cf0c56043
--- /dev/null
+++ b/tika-serialization/src/test/java/org/apache/tika/sax/UppercasingContentHandlerFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.sax;
+
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+import org.xml.sax.ContentHandler;
+
+import org.apache.tika.config.TikaComponent;
+
+/**
+ * A ContentHandlerFactory that creates UppercasingContentHandler instances.
+ * This factory wraps a ToTextContentHandler with an uppercasing decorator
+ * to convert all extracted text to uppercase.
+ * <p>
+ * Used for testing custom ContentHandlerFactory configurations in tika-pipes.
+ */
+@TikaComponent(contextKey = ContentHandlerFactory.class)
+public class UppercasingContentHandlerFactory implements ContentHandlerFactory {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public ContentHandler getNewContentHandler() {
+ return new UppercasingContentHandler(new ToTextContentHandler());
+ }
+
+ @Override
+ public ContentHandler getNewContentHandler(OutputStream os, Charset charset) {
+ try {
+ return new UppercasingContentHandler(new ToTextContentHandler(os, charset.name()));
+ } catch (java.io.UnsupportedEncodingException e) {
+ // Should never happen since we're using a valid Charset
+ throw new RuntimeException("Unexpected encoding error", e);
+ }
+ }
+}
diff --git a/tika-serialization/src/test/java/org/apache/tika/serialization/ConfigDeserializerTest.java b/tika-serialization/src/test/java/org/apache/tika/serialization/ConfigDeserializerTest.java
index 00bd55a2c..ead8d15da 100644
--- a/tika-serialization/src/test/java/org/apache/tika/serialization/ConfigDeserializerTest.java
+++ b/tika-serialization/src/test/java/org/apache/tika/serialization/ConfigDeserializerTest.java
@@ -25,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.ParseContextConfig;
import org.apache.tika.parser.ParseContext;
@@ -79,9 +78,7 @@ public class ConfigDeserializerTest {
// Create ParseContext with user config that overrides some values
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-config", "{\"name\":\"override\",\"value\":200}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("test-config", "{\"name\":\"override\",\"value\":200}");
// Get merged config
TestConfig mergedConfig = ConfigDeserializer.getConfig(context, "test-config", TestConfig.class, defaultConfig);
@@ -111,17 +108,13 @@ public class ConfigDeserializerTest {
// First request with one override
ParseContext context1 = new ParseContext();
- ConfigContainer configContainer1 = new ConfigContainer();
- configContainer1.set("test-config", "{\"value\":100}");
- context1.set(ConfigContainer.class, configContainer1);
+ context1.setJsonConfig("test-config", "{\"value\":100}");
TestConfig config1 = ConfigDeserializer.getConfig(context1, "test-config", TestConfig.class, sharedDefault);
// Second request with different override
ParseContext context2 = new ParseContext();
- ConfigContainer configContainer2 = new ConfigContainer();
- configContainer2.set("test-config", "{\"name\":\"request2\",\"enabled\":false}");
- context2.set(ConfigContainer.class, configContainer2);
+ context2.setJsonConfig("test-config", "{\"name\":\"request2\",\"enabled\":false}");
TestConfig config2 = ConfigDeserializer.getConfig(context2, "test-config", TestConfig.class, sharedDefault);
@@ -148,9 +141,7 @@ public class ConfigDeserializerTest {
@Test
public void testNoDefaultConfig() throws Exception {
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-config", "{\"name\":\"test\",\"value\":123}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("test-config", "{\"name\":\"test\",\"value\":123}");
TestConfig config = ConfigDeserializer.getConfig(context, "test-config", TestConfig.class, null);
@@ -166,7 +157,7 @@ public class ConfigDeserializerTest {
defaultConfig.setName("default");
defaultConfig.setValue(999);
- // No ConfigContainer in ParseContext
+ // No JSON config in ParseContext
ParseContext context = new ParseContext();
TestConfig config = ConfigDeserializer.getConfig(context, "test-config", TestConfig.class, defaultConfig);
@@ -178,12 +169,12 @@ public class ConfigDeserializerTest {
}
@Test
- public void testNoConfigContainer() throws Exception {
+ public void testNoJsonConfig() throws Exception {
TestConfig defaultConfig = new TestConfig();
defaultConfig.setName("default");
ParseContext context = new ParseContext();
- // No ConfigContainer set
+ // No JSON config set
TestConfig config = ConfigDeserializer.getConfig(context, "test-config", TestConfig.class, defaultConfig);
@@ -193,10 +184,8 @@ public class ConfigDeserializerTest {
@Test
public void testHasConfig() throws Exception {
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("parser-a", "{\"key\":\"value\"}");
- configContainer.set("parser-b", "{\"key\":\"value\"}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("parser-a", "{\"key\":\"value\"}");
+ context.setJsonConfig("parser-b", "{\"key\":\"value\"}");
assertTrue(ConfigDeserializer.hasConfig(context, "parser-a"));
assertTrue(ConfigDeserializer.hasConfig(context, "parser-b"));
@@ -204,7 +193,7 @@ public class ConfigDeserializerTest {
}
@Test
- public void testHasConfigNoContainer() throws Exception {
+ public void testHasConfigNoJsonConfigs() throws Exception {
ParseContext context = new ParseContext();
assertFalse(ConfigDeserializer.hasConfig(context, "parser-a"));
@@ -227,9 +216,7 @@ public class ConfigDeserializerTest {
@Test
public void testGetConfigWithoutDefault() throws Exception {
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-config", "{\"name\":\"test\"}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("test-config", "{\"name\":\"test\"}");
TestConfig config = ConfigDeserializer.getConfig(context, "test-config", TestConfig.class);
@@ -256,9 +243,7 @@ public class ConfigDeserializerTest {
defaultConfig.setValue(100);
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("test-parser", "{\"name\":\"override\",\"value\":200}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("test-parser", "{\"name\":\"override\",\"value\":200}");
// Use the wrapper
TestConfig config = ParseContextConfig.getConfig(context, "test-parser", TestConfig.class, defaultConfig);
@@ -289,15 +274,14 @@ public class ConfigDeserializerTest {
@Test
public void testParseContextConfigWrapperIsAvailable() {
// Verify ConfigDeserializer is detected as available in this test environment
- assertTrue(ParseContextConfig.isConfigDeserializerAvailable(), "ConfigDeserializer should be available when tika-serialization is on classpath");
+ assertTrue(ParseContextConfig.isConfigDeserializerAvailable(),
+ "ConfigDeserializer should be available when tika-serialization is on classpath");
}
@Test
public void testParseContextConfigWrapperHasConfig() {
ParseContext context = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("parser-a", "{\"key\":\"value\"}");
- context.set(ConfigContainer.class, configContainer);
+ context.setJsonConfig("parser-a", "{\"key\":\"value\"}");
assertTrue(ParseContextConfig.hasConfig(context, "parser-a"));
assertFalse(ParseContextConfig.hasConfig(context, "parser-b"));
diff --git a/tika-serialization/src/test/java/org/apache/tika/serialization/CustomClassSerializationTest.java b/tika-serialization/src/test/java/org/apache/tika/serialization/CustomClassSerializationTest.java
index bd530a639..ad70cb521 100644
--- a/tika-serialization/src/test/java/org/apache/tika/serialization/CustomClassSerializationTest.java
+++ b/tika-serialization/src/test/java/org/apache/tika/serialization/CustomClassSerializationTest.java
@@ -20,11 +20,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import java.io.StringWriter;
-import java.io.Writer;
import java.util.Locale;
-import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.jupiter.api.Test;
@@ -33,29 +30,20 @@ import org.apache.tika.config.loader.TikaObjectMapperFactory;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+import org.apache.tika.serialization.serdes.ParseContextSerializer;
/**
- * Tests that users can serialize their own custom classes in ParseContext
- * by adding a META-INF/tika-serialization-allowlist.txt file to their JAR.
- *
- * <p>Custom classes MUST implement Serializable because the Pipes parser
- * uses Java serialization to pass ParseContext between processes.</p>
- *
- * <p>To enable JSON serialization of custom classes:</p>
- * <ol>
- * <li>Implement Serializable</li>
- * <li>Provide a no-arg constructor</li>
- * <li>Follow JavaBean conventions (getters/setters)</li>
- * <li>Add your package prefix to META-INF/tika-serialization-allowlist.txt</li>
- * </ol>
+ * Tests that custom classes can be serialized/deserialized via JSON configs.
+ * <p>
+ * With the jsonConfigs approach, serialization works via JSON strings stored
+ * in ParseContext. Custom classes are deserialized at runtime.
*/
public class CustomClassSerializationTest {
/**
* Example custom metadata filter that uppercases all values.
* This simulates a user's custom class (e.g., in package com.acme).
- *
- * <p>Note: Extends Serializable MetadataFilter - this is REQUIRED for use with Pipes parser.</p>
*/
public static class MyUpperCasingMetadataFilter extends MetadataFilter {
private String prefix = "";
@@ -113,49 +101,51 @@ public class CustomClassSerializationTest {
}
@Test
- public void testCustomMetadataFilterSerialization() throws Exception {
- // Create a custom metadata filter
- MyUpperCasingMetadataFilter customFilter = new MyUpperCasingMetadataFilter("TEST_");
-
- // Put it in ParseContext - store as MetadataFilter (the abstract base type)
+ public void testJsonConfigRoundTrip() throws Exception {
+ // Create a ParseContext with JSON config
ParseContext pc = new ParseContext();
- pc.set(MetadataFilter.class, (MetadataFilter) customFilter);
+ pc.setJsonConfig("my-filter", "{\"prefix\":\"TEST_\"}");
// Serialize
ObjectMapper mapper = createMapper();
- String json;
- try (Writer writer = new StringWriter()) {
- try (JsonGenerator jsonGenerator = mapper
- .getFactory()
- .createGenerator(writer)) {
- ParseContextSerializer serializer = new ParseContextSerializer();
- serializer.serialize(pc, jsonGenerator, null);
- }
- json = writer.toString();
- }
+ String json = mapper.writeValueAsString(pc);
- // Verify JSON contains type information
- assertTrue(json.contains("MyUpperCasingMetadataFilter"),
- "JSON should contain the custom class name");
+ // Verify JSON contains the config
+ assertTrue(json.contains("my-filter"), "JSON should contain the config key");
+ assertTrue(json.contains("TEST_"), "JSON should contain the prefix value");
// Deserialize
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
- MetadataFilter deserializedFilter = deserialized.get(MetadataFilter.class);
- // Verify polymorphic deserialization worked - we get back the concrete type
- assertNotNull(deserializedFilter, "MetadataFilter should not be null");
- assertTrue(deserializedFilter instanceof MyUpperCasingMetadataFilter,
- "Filter should be MyUpperCasingMetadataFilter (polymorphic deserialization)");
+ // Verify the JSON config is preserved
+ assertTrue(deserialized.hasJsonConfig("my-filter"));
+ String deserializedJson = deserialized.getJsonConfig("my-filter").json();
+ assertTrue(deserializedJson.contains("TEST_"));
+ }
+
+ @Test
+ public void testCustomClassDeserialization() throws Exception {
+ // Test that a custom class can be deserialized from JSON
+ // This is done via ConfigDeserializer.getConfig() which uses Jackson
+ ParseContext pc = new ParseContext();
+ pc.setJsonConfig("my-filter", "{\"prefix\":\"CUSTOM_\"}");
+
+ // Use ConfigDeserializer to get and deserialize the config
+ // This simulates what a custom component would do
+ ObjectMapper mapper = createMapper();
+ String jsonConfig = pc.getJsonConfig("my-filter").json();
+
+ MyUpperCasingMetadataFilter filter = mapper.readValue(jsonConfig, MyUpperCasingMetadataFilter.class);
- MyUpperCasingMetadataFilter typedFilter = (MyUpperCasingMetadataFilter) deserializedFilter;
- assertEquals("TEST_", typedFilter.getPrefix(), "Prefix should be preserved");
+ assertNotNull(filter);
+ assertEquals("CUSTOM_", filter.getPrefix());
- // Verify it works
+ // Verify the filter works
Metadata metadata = new Metadata();
metadata.add("test", "value");
java.util.List<Metadata> metadataList = new java.util.ArrayList<>();
metadataList.add(metadata);
- typedFilter.filter(metadataList);
- assertEquals("TEST_VALUE", metadata.get("test"), "Filter should uppercase with prefix");
+ filter.filter(metadataList);
+ assertEquals("CUSTOM_VALUE", metadata.get("test"));
}
}
diff --git a/tika-serialization/src/test/java/org/apache/tika/serialization/RoundTripSerializationTest.java b/tika-serialization/src/test/java/org/apache/tika/serialization/RoundTripSerializationTest.java
new file mode 100644
index 000000000..93f792bd1
--- /dev/null
+++ b/tika-serialization/src/test/java/org/apache/tika/serialization/RoundTripSerializationTest.java
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.config.TikaTaskTimeout;
+import org.apache.tika.config.loader.TikaObjectMapperFactory;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+import org.apache.tika.serialization.serdes.ParseContextSerializer;
+
+/**
+ * Tests for round-trip serialization of ParseContext.
+ * Verifies that JSON configs can be serialized and deserialized
+ * with all values preserved.
+ */
+public class RoundTripSerializationTest {
+
+ private ObjectMapper mapper;
+
+ @BeforeEach
+ void setUp() {
+ mapper = TikaObjectMapperFactory.getMapper();
+ SimpleModule module = new SimpleModule();
+ module.addDeserializer(ParseContext.class, new ParseContextDeserializer());
+ module.addSerializer(ParseContext.class, new ParseContextSerializer());
+ mapper.registerModule(module);
+ }
+
+ // ==================== Basic Round-Trip Tests ====================
+
+ @Test
+ void testEmptyContextRoundTrip() throws Exception {
+ ParseContext original = new ParseContext();
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ assertTrue(reloaded.getJsonConfigs().isEmpty());
+ }
+
+ @Test
+ void testSingleConfigRoundTrip() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("pdf-parser", "{\"ocrStrategy\":\"AUTO\",\"extractInlineImages\":true}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ assertTrue(reloaded.hasJsonConfig("pdf-parser"));
+ String reloadedConfig = reloaded.getJsonConfig("pdf-parser").json();
+ assertTrue(reloadedConfig.contains("AUTO"));
+ assertTrue(reloadedConfig.contains("extractInlineImages"));
+ }
+
+ @Test
+ void testMultipleConfigsRoundTrip() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("pdf-parser", "{\"ocrStrategy\":\"AUTO\"}");
+ original.setJsonConfig("html-parser", "{\"extractScripts\":false}");
+ original.setJsonConfig("tika-task-timeout", "{\"timeoutMillis\":30000}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ assertEquals(3, reloaded.getJsonConfigs().size());
+ assertTrue(reloaded.hasJsonConfig("pdf-parser"));
+ assertTrue(reloaded.hasJsonConfig("html-parser"));
+ assertTrue(reloaded.hasJsonConfig("tika-task-timeout"));
+ }
+
+ @Test
+ void testNestedJsonRoundTrip() throws Exception {
+ ParseContext original = new ParseContext();
+ String nestedJson = """
+ {"level1":{"level2":{"array":[1,2,3],"nested":{"deep":"value"}}}}
+ """.trim();
+ original.setJsonConfig("complex-config", nestedJson);
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ String reloadedConfig = reloaded.getJsonConfig("complex-config").json();
+ JsonNode node = mapper.readTree(reloadedConfig);
+ assertEquals("value", node.get("level1").get("level2").get("nested").get("deep").asText());
+ assertEquals(3, node.get("level1").get("level2").get("array").size());
+ }
+
+ // ==================== Multiple Round-Trips Stability ====================
+
+ @Test
+ void testMultipleRoundTripsStability() throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("pdf-parser", "{\"ocrStrategy\":\"NO_OCR\"}");
+ context.setJsonConfig("tika-task-timeout", "{\"timeoutMillis\":45000}");
+
+ // Perform 5 round-trips
+ for (int i = 0; i < 5; i++) {
+ String json = mapper.writeValueAsString(context);
+ context = mapper.readValue(json, ParseContext.class);
+ }
+
+ // Verify values preserved after multiple round-trips
+ assertEquals(2, context.getJsonConfigs().size());
+ assertTrue(context.getJsonConfig("pdf-parser").json().contains("NO_OCR"));
+ assertTrue(context.getJsonConfig("tika-task-timeout").json().contains("45000"));
+ }
+
+ // ==================== TikaTaskTimeout Round-Trip ====================
+
+ @Test
+ void testTikaTaskTimeoutRoundTrip() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("tika-task-timeout", "{\"timeoutMillis\":60000}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ // Resolve and verify
+ ParseContextUtils.resolveAll(reloaded, Thread.currentThread().getContextClassLoader());
+ TikaTaskTimeout timeout = reloaded.get(TikaTaskTimeout.class);
+
+ assertNotNull(timeout);
+ assertEquals(60000, timeout.getTimeoutMillis());
+ }
+
+ @Test
+ void testTikaTaskTimeoutDifferentValues() throws Exception {
+ long[] testValues = {1000, 5000, 30000, 120000, 600000};
+
+ for (long value : testValues) {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("tika-task-timeout", "{\"timeoutMillis\":" + value + "}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ ParseContextUtils.resolveAll(reloaded, Thread.currentThread().getContextClassLoader());
+ TikaTaskTimeout timeout = reloaded.get(TikaTaskTimeout.class);
+
+ assertEquals(value, timeout.getTimeoutMillis(),
+ "Timeout " + value + " should survive round-trip");
+ }
+ }
+
+ // ==================== JSON Structure Preservation ====================
+
+ @Test
+ void testArrayValuesPreserved() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("test-config", "{\"items\":[\"a\",\"b\",\"c\"],\"numbers\":[1,2,3]}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ JsonNode node = mapper.readTree(reloaded.getJsonConfig("test-config").json());
+ assertEquals(3, node.get("items").size());
+ assertEquals("b", node.get("items").get(1).asText());
+ assertEquals(2, node.get("numbers").get(1).asInt());
+ }
+
+ @Test
+ void testBooleanValuesPreserved() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("test-config", "{\"enabled\":true,\"disabled\":false}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ JsonNode node = mapper.readTree(reloaded.getJsonConfig("test-config").json());
+ assertTrue(node.get("enabled").asBoolean());
+ assertFalse(node.get("disabled").asBoolean());
+ }
+
+ @Test
+ void testNullValuesPreserved() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("test-config", "{\"value\":null,\"other\":\"text\"}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ JsonNode node = mapper.readTree(reloaded.getJsonConfig("test-config").json());
+ assertTrue(node.get("value").isNull());
+ assertEquals("text", node.get("other").asText());
+ }
+
+ // ==================== parseContext Wrapper Format ====================
+
+ @Test
+ void testParseContextWrapperFormat() throws Exception {
+ // Test that we can deserialize from wrapped format
+ String wrappedJson = """
+ {
+ "parseContext": {
+ "pdf-parser": {"ocrStrategy": "AUTO"}
+ }
+ }
+ """;
+
+ ParseContext reloaded = mapper.readValue(wrappedJson, ParseContext.class);
+
+ assertTrue(reloaded.hasJsonConfig("pdf-parser"));
+ assertTrue(reloaded.getJsonConfig("pdf-parser").json().contains("AUTO"));
+ }
+
+ @Test
+ void testFlatFormatPreferred() throws Exception {
+ // Verify serialization uses flat format (no wrapper)
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("pdf-parser", "{\"ocrStrategy\":\"AUTO\"}");
+
+ String json = mapper.writeValueAsString(original);
+
+ assertFalse(json.contains("parseContext"), "Should serialize in flat format");
+ assertTrue(json.contains("pdf-parser"));
+ }
+
+ // ==================== Edge Cases ====================
+
+ @Test
+ void testEmptyObjectConfig() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("empty-config", "{}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ assertTrue(reloaded.hasJsonConfig("empty-config"));
+ assertEquals("{}", reloaded.getJsonConfig("empty-config").json());
+ }
+
+ @Test
+ void testSpecialCharactersInValues() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("test-config", "{\"path\":\"/path/to/file\",\"regex\":\"\\\\d+\"}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ JsonNode node = mapper.readTree(reloaded.getJsonConfig("test-config").json());
+ assertEquals("/path/to/file", node.get("path").asText());
+ }
+
+ @Test
+ void testUnicodeInValues() throws Exception {
+ ParseContext original = new ParseContext();
+ original.setJsonConfig("test-config", "{\"message\":\"Hello δΈη\",\"emoji\":\"π\"}");
+
+ String json = mapper.writeValueAsString(original);
+ ParseContext reloaded = mapper.readValue(json, ParseContext.class);
+
+ JsonNode node = mapper.readTree(reloaded.getJsonConfig("test-config").json());
+ assertEquals("Hello δΈη", node.get("message").asText());
+ assertEquals("π", node.get("emoji").asText());
+ }
+}
diff --git a/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java b/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java
index 4a300830c..b53c7df13 100644
--- a/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java
+++ b/tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java
@@ -23,8 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.StringWriter;
import java.io.Writer;
-import java.util.List;
-import java.util.Set;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
@@ -32,23 +30,23 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.jupiter.api.Test;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.TikaTaskTimeout;
import org.apache.tika.config.loader.TikaObjectMapperFactory;
import org.apache.tika.extractor.DocumentSelector;
import org.apache.tika.extractor.SkipEmbeddedDocumentSelector;
import org.apache.tika.metadata.filter.AttachmentCountingListFilter;
import org.apache.tika.metadata.filter.CompositeMetadataFilter;
-import org.apache.tika.metadata.filter.IncludeFieldMetadataFilter;
import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.metadata.filter.MockUpperCaseFilter;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
+import org.apache.tika.serialization.serdes.ParseContextSerializer;
/**
* Tests for ParseContext serialization/deserialization.
* <p>
- * All configs use friendly names and are stored in ConfigContainer.
- * Components are resolved at runtime via ParseContextUtils.resolveAll().
+ * JSON configs are stored in ParseContext's jsonConfigs map.
+ * Components are resolved at runtime via ParseContextUtils.resolveAll() or ConfigDeserializer.
*/
public class TestParseContextSerialization {
@@ -96,17 +94,14 @@ public class TestParseContextSerialization {
public void testFriendlyNameFormat() throws Exception {
// Test the friendly-name format
ParseContext pc = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- // Add friendly-named configurations
- configContainer.set("pdf-parser", "{\"ocrStrategy\":\"AUTO\",\"extractInlineImages\":true}");
- configContainer.set("html-parser", "{\"extractScripts\":false}");
-
- pc.set(ConfigContainer.class, configContainer);
+ // Add friendly-named configurations via setJsonConfig
+ pc.setJsonConfig("pdf-parser", "{\"ocrStrategy\":\"AUTO\",\"extractInlineImages\":true}");
+ pc.setJsonConfig("html-parser", "{\"extractScripts\":false}");
String json = serializeParseContext(pc);
- // Verify JSON structure - should have flat friendly names, no "objects" wrapper
+ // Verify JSON structure - should have flat friendly names
ObjectMapper mapper = createMapper();
JsonNode root = mapper.readTree(json);
@@ -124,24 +119,15 @@ public class TestParseContextSerialization {
// Verify round-trip
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
- ConfigContainer deserializedConfig = deserialized.get(ConfigContainer.class);
- assertNotNull(deserializedConfig);
- assertTrue(deserializedConfig
- .get("pdf-parser")
- .isPresent());
- assertTrue(deserializedConfig
- .get("html-parser")
- .isPresent());
+ assertTrue(deserialized.hasJsonConfig("pdf-parser"));
+ assertTrue(deserialized.hasJsonConfig("html-parser"));
}
@Test
public void testTikaTaskTimeoutFormat() throws Exception {
// Test serializing tika-task-timeout configuration
ParseContext pc = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
-
- configContainer.set("tika-task-timeout", "{\"timeoutMillis\":30000}");
- pc.set(ConfigContainer.class, configContainer);
+ pc.setJsonConfig("tika-task-timeout", "{\"timeoutMillis\":30000}");
String json = serializeParseContext(pc);
@@ -154,11 +140,14 @@ public class TestParseContextSerialization {
.get("timeoutMillis")
.asInt());
- // Verify round-trip - TikaTaskTimeout is NOT SelfConfiguring,
- // so it gets resolved directly into ParseContext (not ConfigContainer)
+ // Verify round-trip
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
+ assertTrue(deserialized.hasJsonConfig("tika-task-timeout"));
+
+ // Resolve and verify
+ ParseContextUtils.resolveAll(deserialized, Thread.currentThread().getContextClassLoader());
TikaTaskTimeout timeout = deserialized.get(TikaTaskTimeout.class);
- assertNotNull(timeout, "TikaTaskTimeout should be resolved directly into ParseContext");
+ assertNotNull(timeout, "TikaTaskTimeout should be resolved");
assertEquals(30000, timeout.getTimeoutMillis());
}
@@ -166,22 +155,17 @@ public class TestParseContextSerialization {
public void testConfigDeserializerHelper() throws Exception {
// Test the ConfigDeserializer helper utility
ParseContext pc = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
// Simulate a PDFParserConfig as JSON
String pdfConfig = "{\"extractInlineImages\":true,\"ocrStrategy\":\"AUTO\"}";
- configContainer.set("pdf-parser", pdfConfig);
-
- pc.set(ConfigContainer.class, configContainer);
+ pc.setJsonConfig("pdf-parser", pdfConfig);
// Test hasConfig
assertTrue(ConfigDeserializer.hasConfig(pc, "pdf-parser"));
assertFalse(ConfigDeserializer.hasConfig(pc, "non-existent"));
- // Test getConfig retrieves JSON correctly
- String retrievedConfig = pc
- .get(ConfigContainer.class)
- .get("pdf-parser").get().json();
+ // Test getJsonConfig retrieves JSON correctly
+ String retrievedConfig = pc.getJsonConfig("pdf-parser").json();
assertNotNull(retrievedConfig);
assertTrue(retrievedConfig.contains("extractInlineImages"));
}
@@ -204,18 +188,11 @@ public class TestParseContextSerialization {
ObjectMapper mapper = createMapper();
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
- ConfigContainer config = deserialized.get(ConfigContainer.class);
- assertNotNull(config);
- assertTrue(config
- .get("pdf-parser")
- .isPresent());
- assertTrue(config
- .get("html-parser")
- .isPresent());
+ assertTrue(deserialized.hasJsonConfig("pdf-parser"));
+ assertTrue(deserialized.hasJsonConfig("html-parser"));
// Verify the JSON content is preserved
- String pdfParserJson = config
- .get("pdf-parser").get().json();
+ String pdfParserJson = deserialized.getJsonConfig("pdf-parser").json();
assertTrue(pdfParserJson.contains("AUTO"));
assertTrue(pdfParserJson.contains("extractInlineImages"));
}
@@ -236,25 +213,18 @@ public class TestParseContextSerialization {
ObjectMapper mapper = createMapper();
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
- ConfigContainer config = deserialized.get(ConfigContainer.class);
- assertNotNull(config);
- assertTrue(config
- .get("pdf-parser")
- .isPresent());
+ assertTrue(deserialized.hasJsonConfig("pdf-parser"));
}
@Test
public void testMultipleConfigs() throws Exception {
// Test with multiple different config types
ParseContext pc = new ParseContext();
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("pdf-parser", "{\"ocrStrategy\":\"AUTO\"}");
- configContainer.set("html-parser", "{\"extractScripts\":true}");
- configContainer.set("tika-task-timeout", "{\"timeoutMillis\":5000}");
- configContainer.set("my-custom-config", "{\"enabled\":true,\"maxRetries\":3}");
-
- pc.set(ConfigContainer.class, configContainer);
+ pc.setJsonConfig("pdf-parser", "{\"ocrStrategy\":\"AUTO\"}");
+ pc.setJsonConfig("html-parser", "{\"extractScripts\":true}");
+ pc.setJsonConfig("tika-task-timeout", "{\"timeoutMillis\":5000}");
+ pc.setJsonConfig("my-custom-config", "{\"enabled\":true,\"maxRetries\":3}");
String json = serializeParseContext(pc);
@@ -269,84 +239,51 @@ public class TestParseContextSerialization {
assertTrue(root.has("my-custom-config"));
// Verify round-trip
- // After deserialization:
- // - pdf-parser, html-parser β Parsers are SelfConfiguring β stay in ConfigContainer
- // - my-custom-config β not in registry β stays in ConfigContainer
- // - tika-task-timeout β TikaTaskTimeout is NOT SelfConfiguring β resolved directly
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
- ConfigContainer deserializedConfig = deserialized.get(ConfigContainer.class);
- assertEquals(3, deserializedConfig.getKeys().size(),
- "Should have 3 configs in ConfigContainer (SelfConfiguring + unknown)");
- assertTrue(deserializedConfig.get("pdf-parser").isPresent());
- assertTrue(deserializedConfig.get("html-parser").isPresent());
- assertTrue(deserializedConfig.get("my-custom-config").isPresent());
-
- // TikaTaskTimeout should be resolved directly into ParseContext
- TikaTaskTimeout timeout = deserialized.get(TikaTaskTimeout.class);
- assertNotNull(timeout, "TikaTaskTimeout should be resolved directly");
- assertEquals(5000, timeout.getTimeoutMillis());
+ assertEquals(4, deserialized.getJsonConfigs().size());
}
@Test
- public void testProgrammaticObjectsWithoutFriendlyName() throws Exception {
- // Objects without a registered friendly name are NOT serialized
+ public void testProgrammaticObjectsNotSerialized() throws Exception {
+ // Typed objects set via context.set() are NOT serialized
+ // Only jsonConfigs are serialized for clean round-trip
ParseContext pc = new ParseContext();
- // String doesn't have a @TikaComponent annotation, so it won't serialize
+ // String doesn't have a @TikaComponent annotation
pc.set(String.class, "test-value");
String json = serializeParseContext(pc);
- // Should be empty - String doesn't have a friendly name
+ // Should be empty - typed objects are not serialized
ObjectMapper mapper = createMapper();
JsonNode root = mapper.readTree(json);
- assertEquals(0, root.size(), "Objects without friendly names should not be serialized");
+ assertEquals(0, root.size(), "Typed objects should not be serialized");
}
@Test
- public void testMetadataListConfigContainer() throws Exception {
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("metadata-filters", """
+ public void testMetadataFiltersFromJson() throws Exception {
+ ParseContext parseContext = new ParseContext();
+ parseContext.setJsonConfig("metadata-filters", """
[
"attachment-counting-list-filter",
"mock-upper-case-filter"
]
""");
- ParseContext parseContext = new ParseContext();
- parseContext.set(ConfigContainer.class, configContainer);
ObjectMapper mapper = createMapper();
String json = mapper.writeValueAsString(parseContext);
ParseContext deser = mapper.readValue(json, ParseContext.class);
- MetadataFilter resolvedFilter = deser.get(MetadataFilter.class);
- assertNotNull(resolvedFilter, "MetadataFilter should be resolved");
- assertEquals(CompositeMetadataFilter.class, resolvedFilter.getClass());
- CompositeMetadataFilter deserFilter = (CompositeMetadataFilter) resolvedFilter;
- assertEquals(AttachmentCountingListFilter.class, deserFilter.getFilters().get(0).getClass());
- }
-
- @Test
- public void testMetadataListPOJO() throws Exception {
- CompositeMetadataFilter metadataFilter =
- new CompositeMetadataFilter(List.of(new MockUpperCaseFilter(), new AttachmentCountingListFilter(),
- new IncludeFieldMetadataFilter(Set.of("blah", "blah2"))));
+ // Resolve the array config
+ ParseContextUtils.resolveAll(deser, Thread.currentThread().getContextClassLoader());
- ParseContext parseContext = new ParseContext();
- parseContext.set(MetadataFilter.class, metadataFilter);
-
- ObjectMapper mapper = createMapper();
- String json = mapper.writeValueAsString(parseContext);
-
- ParseContext deser = mapper.readValue(json, ParseContext.class);
MetadataFilter resolvedFilter = deser.get(MetadataFilter.class);
assertNotNull(resolvedFilter, "MetadataFilter should be resolved");
assertEquals(CompositeMetadataFilter.class, resolvedFilter.getClass());
CompositeMetadataFilter deserFilter = (CompositeMetadataFilter) resolvedFilter;
- assertEquals(MockUpperCaseFilter.class, deserFilter.getFilters().get(0).getClass());
- assertEquals(AttachmentCountingListFilter.class, deserFilter.getFilters().get(1).getClass());
- assertEquals(IncludeFieldMetadataFilter.class, deserFilter.getFilters().get(2).getClass());
+ assertEquals(AttachmentCountingListFilter.class, deserFilter.getFilters().get(0).getClass());
+ assertEquals(MockUpperCaseFilter.class, deserFilter.getFilters().get(1).getClass());
}
@Test
@@ -363,6 +300,9 @@ public class TestParseContextSerialization {
ObjectMapper mapper = createMapper();
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
+ // Resolve the config
+ ParseContextUtils.resolveAll(deserialized, Thread.currentThread().getContextClassLoader());
+
// Should be accessible via DocumentSelector.class (the contextKey)
DocumentSelector selector = deserialized.get(DocumentSelector.class);
assertNotNull(selector, "DocumentSelector should be found via contextKey");
diff --git a/tika-serialization/src/test/resources/configs/TIKA-3695.json b/tika-serialization/src/test/resources/configs/TIKA-3695.json
index 29855915d..f7c05313b 100644
--- a/tika-serialization/src/test/resources/configs/TIKA-3695.json
+++ b/tika-serialization/src/test/resources/configs/TIKA-3695.json
@@ -7,7 +7,8 @@
"outputThreshold": 6789,
"metadataWriteFilterFactory": {
"standard-write-filter-factory": {
- "maxTotalEstimatedBytes": 360
+ "maxTotalEstimatedBytes": 150,
+ "includeFields": ["dc:creator"]
}
}
}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
index 00489d975..71327764a 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
@@ -78,7 +78,7 @@ import org.apache.tika.sax.BodyContentHandler;
import org.apache.tika.sax.ExpandedTitleContentHandler;
import org.apache.tika.sax.RichTextContentHandler;
import org.apache.tika.sax.boilerpipe.BoilerpipeContentHandler;
-import org.apache.tika.serialization.ParseContextDeserializer;
+import org.apache.tika.serialization.serdes.ParseContextDeserializer;
import org.apache.tika.serialization.ParseContextUtils;
import org.apache.tika.server.core.InputStreamFactory;
import org.apache.tika.server.core.ServerStatus;