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/10 22:29:08 UTC
(tika) branch TIKA-4562 created (now a39239c1c1)
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a change to branch TIKA-4562
in repository https://gitbox.apache.org/repos/asf/tika.git
at a39239c1c1 TIKA-4562 -- kit and kaboodle
This branch includes the following new commits:
new 6964ee6704 TIKA-4562 -- checkpoint commit
new a39239c1c1 TIKA-4562 -- kit and kaboodle
The 2 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) 01/02: TIKA-4562 -- checkpoint commit
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4562
in repository https://gitbox.apache.org/repos/asf/tika.git
commit 6964ee6704eccaca01f3e0fcf3922e64b6abebc5
Author: tallison <ta...@apache.org>
AuthorDate: Wed Dec 10 11:26:53 2025 -0500
TIKA-4562 -- checkpoint commit
---
.../tika/annotation/TikaComponentProcessor.java | 82 +++++++--
.../java/org/apache/tika/config/TikaComponent.java | 25 +++
.../org/apache/tika/config/ConfigContainer.java | 5 +-
.../org/apache/tika/config/TikaTaskTimeout.java | 30 +++-
.../java/org/apache/tika/parser/ParseContext.java | 28 ++++
.../org/apache/tika/pipes/api/HandlerConfig.java | 4 +-
.../org/apache/tika/pipes/core/PipesClient.java | 2 +-
.../apache/tika/pipes/core/server/PipesServer.java | 3 +
.../apache/tika/config/loader/ComponentInfo.java | 38 +++--
.../tika/config/loader/ComponentRegistry.java | 102 +++++++++--
.../apache/tika/config/loader/ConfigLoader.java | 11 +-
.../apache/tika/config/loader/JsonMergeUtils.java | 104 ++++++++++++
.../tika/serialization/ConfigDeserializer.java | 15 +-
.../serialization/ParseContextDeserializer.java | 57 +++----
.../tika/serialization/ParseContextSerializer.java | 104 ++++++++----
.../tika/serialization/ParseContextUtils.java | 186 +++++++++++++++++++++
.../TestParseContextSerialization.java | 181 ++++++++++----------
tika-server/tika-server-core/pom.xml | 5 +
.../tika/server/core/ServerStatusWatcher.java | 9 +-
.../server/core/resource/DetectorResource.java | 1 +
.../tika/server/core/resource/PipesResource.java | 3 +
.../tika/server/core/resource/TikaResource.java | 135 ++++++++++++---
.../org/apache/tika/server/core/TikaPipesTest.java | 1 -
.../server/core/TikaServerIntegrationTest.java | 56 +++++--
.../core/TikaServerPipesIntegrationTest.java | 44 +++++
.../configs/tika-config-server-basic.json | 3 +-
26 files changed, 957 insertions(+), 277 deletions(-)
diff --git a/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java b/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java
index e85a2d1f6e..f1f79943ee 100644
--- a/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java
+++ b/tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java
@@ -34,7 +34,10 @@ import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
@@ -125,17 +128,31 @@ public class TikaComponentProcessor extends AbstractProcessor {
// Check if component should be included in SPI
boolean includeSpi = annotation.spi();
+ // Get contextKey if specified (need to use mirror API for Class types)
+ String contextKey = getContextKeyFromAnnotation(element);
+
messager.printMessage(Diagnostic.Kind.NOTE,
"Processing @TikaComponent: " + className + " -> " + componentName +
- " (SPI: " + includeSpi + ")");
+ " (SPI: " + includeSpi + ", contextKey: " + contextKey + ")");
// Find all implemented service interfaces
List<String> serviceInterfaces = findServiceInterfaces(element);
+ // Build the index entry value (className or className:key=X)
+ String indexValue = className;
+ if (contextKey != null) {
+ indexValue = className + ":key=" + contextKey;
+ }
+
if (serviceInterfaces.isEmpty()) {
- messager.printMessage(Diagnostic.Kind.WARNING,
- "Class " + className + " annotated with @TikaComponent " +
- "but does not implement any known Tika service interface", element);
+ // No known service interface - put in other-configs.idx
+ messager.printMessage(Diagnostic.Kind.NOTE,
+ "Class " + className + " does not implement known service interface, " +
+ "adding to other-configs.idx", element);
+
+ Map<String, String> index = indexFiles.computeIfAbsent("other-configs",
+ k -> new LinkedHashMap<>());
+ addToIndex(index, componentName, indexValue, className, element);
return;
}
@@ -152,20 +169,57 @@ public class TikaComponentProcessor extends AbstractProcessor {
if (indexFileName != null) {
Map<String, String> index = indexFiles.computeIfAbsent(indexFileName,
k -> new LinkedHashMap<>());
+ addToIndex(index, componentName, indexValue, className, element);
+ }
+ }
+ }
- // Check for duplicate names
- if (index.containsKey(componentName)) {
- String existingClass = index.get(componentName);
- if (!existingClass.equals(className)) {
- messager.printMessage(Diagnostic.Kind.ERROR,
- "Duplicate component name '" + componentName + "' for classes: " +
- existingClass + " and " + className, element);
+ /**
+ * Adds an entry to an index, checking for duplicates.
+ */
+ private void addToIndex(Map<String, String> index, String componentName,
+ String indexValue, String className, TypeElement element) {
+ if (index.containsKey(componentName)) {
+ String existingValue = index.get(componentName);
+ // Extract class name from value (may have :key= suffix)
+ String existingClass = existingValue.contains(":")
+ ? existingValue.substring(0, existingValue.indexOf(":"))
+ : existingValue;
+ if (!existingClass.equals(className)) {
+ messager.printMessage(Diagnostic.Kind.ERROR,
+ "Duplicate component name '" + componentName + "' for classes: " +
+ existingClass + " and " + className, element);
+ }
+ } else {
+ index.put(componentName, indexValue);
+ }
+ }
+
+ /**
+ * Gets the contextKey value from the annotation using the mirror API.
+ * Returns null if contextKey is void.class (the default).
+ */
+ private String getContextKeyFromAnnotation(TypeElement element) {
+ for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
+ DeclaredType annotationType = mirror.getAnnotationType();
+ if (annotationType.toString().equals(TikaComponent.class.getName())) {
+ for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry
+ : mirror.getElementValues().entrySet()) {
+ if (entry.getKey().getSimpleName().toString().equals("contextKey")) {
+ // The value is a TypeMirror for Class types
+ Object value = entry.getValue().getValue();
+ if (value instanceof TypeMirror) {
+ String typeName = value.toString();
+ // void.class is the default, meaning "auto-detect"
+ if (!"void".equals(typeName) && !"java.lang.Void".equals(typeName)) {
+ return typeName;
+ }
+ }
}
- } else {
- index.put(componentName, className);
}
}
}
+ return null;
}
/**
@@ -267,7 +321,7 @@ public class TikaComponentProcessor extends AbstractProcessor {
writeApacheLicenseHeader(writer);
writer.write("# Generated by TikaComponentProcessor\n");
writer.write("# Do not edit manually\n");
- writer.write("# Format: component-name=fully.qualified.ClassName\n");
+ writer.write("# Format: component-name=fully.qualified.ClassName[:key=contextKeyClass]\n");
for (Map.Entry<String, String> component : components.entrySet()) {
writer.write(component.getKey());
writer.write("=");
diff --git a/tika-annotation-processor/src/main/java/org/apache/tika/config/TikaComponent.java b/tika-annotation-processor/src/main/java/org/apache/tika/config/TikaComponent.java
index 6632bdeb7f..e7f35814cb 100644
--- a/tika-annotation-processor/src/main/java/org/apache/tika/config/TikaComponent.java
+++ b/tika-annotation-processor/src/main/java/org/apache/tika/config/TikaComponent.java
@@ -53,6 +53,11 @@ import java.lang.annotation.Target;
* public class DWGReadParser extends AbstractParser {
* // available by name, but NOT auto-loaded by default-parser
* }
+ *
+ * {@code @TikaComponent(contextKey = MetadataFilter.class)}
+ * public class MyFilter implements MetadataFilter, AnotherInterface {
+ * // explicit ParseContext key when class implements multiple known interfaces
+ * }
* </pre>
*
* @since 3.1.0
@@ -81,4 +86,24 @@ public @interface TikaComponent {
* @return true to include in SPI (default), false to require explicit config
*/
boolean spi() default true;
+
+ /**
+ * The class to use as the key when adding this component to ParseContext.
+ * <p>
+ * By default ({@code void.class}), the key is auto-detected:
+ * <ul>
+ * <li>If the component implements a known interface (e.g., MetadataFilter),
+ * that interface is used as the key</li>
+ * <li>Otherwise, the component's own class is used as the key</li>
+ * </ul>
+ * <p>
+ * Use this attribute to explicitly specify the key when:
+ * <ul>
+ * <li>The component implements multiple known interfaces (ambiguous)</li>
+ * <li>You need a specific interface/class that isn't auto-detected</li>
+ * </ul>
+ *
+ * @return the class to use as ParseContext key, or void.class for auto-detection
+ */
+ Class<?> contextKey() default void.class;
}
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
index abc668f73d..f4eedd5bb2 100644
--- a/tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java
+++ b/tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java
@@ -16,6 +16,7 @@
*/
package org.apache.tika.config;
+import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -32,7 +33,9 @@ import java.util.Set;
* 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 {
+public class ConfigContainer implements Serializable {
+
+ private static final long serialVersionUID = 1L;
private final Map<String, String> configs = new HashMap<>();
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 842c7a432d..cf3928fca2 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
@@ -20,10 +20,34 @@ import java.io.Serializable;
import org.apache.tika.parser.ParseContext;
+/**
+ * Configuration class for specifying parse task timeout.
+ * <pre>
+ * {
+ * "parse-context": {
+ * "tika-task-timeout": {
+ * "timeoutMillis": 30000
+ * }
+ * }
+ * }
+ * </pre>
+ */
+@TikaComponent(spi = false)
public class TikaTaskTimeout implements Serializable {
- private final long timeoutMillis;
+ private long timeoutMillis;
+
+ /**
+ * No-arg constructor for Jackson deserialization.
+ */
+ public TikaTaskTimeout() {
+ }
+ /**
+ * Constructor with timeout value.
+ *
+ * @param timeoutMillis timeout in milliseconds
+ */
public TikaTaskTimeout(long timeoutMillis) {
this.timeoutMillis = timeoutMillis;
}
@@ -32,6 +56,10 @@ public class TikaTaskTimeout implements Serializable {
return timeoutMillis;
}
+ public void setTimeoutMillis(long timeoutMillis) {
+ this.timeoutMillis = timeoutMillis;
+ }
+
public static long getTimeoutMillis(ParseContext context, long defaultTimeoutMillis) {
if (context == null) {
return defaultTimeoutMillis;
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 0393bec52c..6e16391c27 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,6 +21,8 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import org.apache.tika.config.ConfigContainer;
+
/**
* Parse context. Used to pass context information to Tika parsers.
*
@@ -84,6 +86,32 @@ public class ParseContext implements Serializable {
}
}
+ /**
+ * Adds a configuration by friendly name for serialization.
+ * <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.
+ * <p>
+ * Example:
+ * <pre>
+ * parseContext.addConfig("tika-task-timeout", "{\"timeoutMillis\": 5000}");
+ * parseContext.addConfig("handler-config", "{\"type\": \"XML\", \"parseMode\": \"RMETA\"}");
+ * </pre>
+ *
+ * @param key the friendly name of the config (e.g., "tika-task-timeout", "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);
+ }
+ container.set(key, json);
+ }
+
public boolean isEmpty() {
return context.isEmpty();
}
diff --git a/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/HandlerConfig.java b/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/HandlerConfig.java
index b58c58c05a..b336f1a4fc 100644
--- a/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/HandlerConfig.java
+++ b/tika-pipes/tika-pipes-api/src/main/java/org/apache/tika/pipes/api/HandlerConfig.java
@@ -22,7 +22,9 @@ import java.util.Objects;
import org.apache.tika.sax.BasicContentHandlerFactory;
-//TODO -- convert this back to a record
+/**
+ * Configuration for content handler behavior during parsing.
+ */
public class HandlerConfig implements Serializable {
/**
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/PipesClient.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/PipesClient.java
index c4a2c07136..1551ae37d9 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/PipesClient.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/PipesClient.java
@@ -194,7 +194,7 @@ public class PipesClient implements Closeable {
} catch (InterruptedException | SecurityException e) {
throw e;
} catch (Exception e) {
- LOG.error("exception waiting for server to complete task: {} ", t.getId());
+ LOG.error("exception waiting for server to complete task: {} ", t.getId(), e);
shutItAllDown();
return buildFatalResult(t.getId(), t.getEmitKey(), UNSPECIFIED_CRASH, intermediateResult.get());
}
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
index e84945b925..c971367ab5 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
@@ -58,6 +58,7 @@ import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.detect.Detector;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
+import org.apache.tika.serialization.ParseContextUtils;
import org.apache.tika.extractor.RUnpackExtractorFactory;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.filter.MetadataFilter;
@@ -286,6 +287,8 @@ public class PipesServer implements AutoCloseable {
CountDownLatch countDownLatch = new CountDownLatch(1);
FetchEmitTuple fetchEmitTuple = readFetchEmitTuple();
+ // Resolve friendly-named configs in ParseContext to actual objects
+ ParseContextUtils.resolveAll(fetchEmitTuple.getParseContext(), getClass().getClassLoader());
PipesWorker pipesWorker = getPipesWorker(intermediateResult, fetchEmitTuple, countDownLatch);
executorCompletionService.submit(pipesWorker);
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/TimeoutConfig.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInfo.java
similarity index 50%
rename from tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/TimeoutConfig.java
rename to tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInfo.java
index 88a9e102f7..72c7156b2c 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/TimeoutConfig.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentInfo.java
@@ -14,24 +14,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.server.core.config;
+package org.apache.tika.config.loader;
-import jakarta.ws.rs.core.MultivaluedMap;
-
-import org.apache.tika.config.TikaTaskTimeout;
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.parser.ParseContext;
-import org.apache.tika.server.core.ParseContextConfig;
-
-public class TimeoutConfig implements ParseContextConfig {
-
- public static final String X_TIKA_TIMEOUT_MILLIS = "X-Tika-Timeout-Millis";
-
- @Override
- public void configure(MultivaluedMap<String, String> httpHeaders, Metadata metadata, ParseContext context) {
- if (httpHeaders.containsKey(X_TIKA_TIMEOUT_MILLIS)) {
- long timeout = Long.parseLong(httpHeaders.getFirst(X_TIKA_TIMEOUT_MILLIS));
- context.set(TikaTaskTimeout.class, new TikaTaskTimeout(timeout));
- }
+/**
+ * Information about a registered Tika component.
+ *
+ * @param componentClass the component's class
+ * @param selfConfiguring whether the component implements SelfConfiguring
+ * (reads its own config from ConfigContainer)
+ * @param contextKey the class to use as the key when adding to ParseContext,
+ * or null to auto-detect based on known interfaces
+ */
+public record ComponentInfo(
+ Class<?> componentClass,
+ boolean selfConfiguring,
+ Class<?> contextKey
+) {
+ /**
+ * Creates a ComponentInfo with no explicit context key (auto-detect).
+ */
+ public ComponentInfo(Class<?> componentClass, boolean selfConfiguring) {
+ this(componentClass, selfConfiguring, null);
}
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java
index a8097caf0c..2978f09513 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java
@@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
+import org.apache.tika.config.SelfConfiguring;
import org.apache.tika.exception.TikaConfigException;
/**
@@ -35,6 +36,13 @@ import org.apache.tika.exception.TikaConfigException;
* Loads component name-to-class mappings from META-INF/tika/*.idx files
* generated by the {@code @TikaComponent} annotation processor.
* <p>
+ * The registry tracks:
+ * <ul>
+ * <li>Component class</li>
+ * <li>Whether the component is self-configuring (implements {@link SelfConfiguring})</li>
+ * <li>Optional explicit context key for ParseContext</li>
+ * </ul>
+ * <p>
* Also includes built-in aliases for external dependencies that cannot be
* annotated with @TikaComponent.
*/
@@ -51,10 +59,13 @@ public class ComponentRegistry {
// Alias for org.gagravarr:vorbis-java-tika dependency
// TODO -- make this configurable
aliases.put("ogg-detector", "org.gagravarr.tika.OggDetector");
+ // HandlerConfig is in tika-pipes-api which can't depend on tika-core for @TikaComponent
+ aliases.put("handler-config", "org.apache.tika.pipes.api.HandlerConfig");
return Collections.unmodifiableMap(aliases);
}
- private final Map<String, Class<?>> components;
+ private final Map<String, ComponentInfo> components;
+ private final Map<Class<?>, String> classToName; // Reverse lookup
private final ClassLoader classLoader;
/**
@@ -69,6 +80,11 @@ public class ComponentRegistry {
throws TikaConfigException {
this.classLoader = classLoader;
this.components = loadComponents(indexFileName);
+ // Build reverse lookup
+ this.classToName = new HashMap<>();
+ for (Map.Entry<String, ComponentInfo> entry : components.entrySet()) {
+ classToName.put(entry.getValue().componentClass(), entry.getKey());
+ }
}
/**
@@ -79,20 +95,32 @@ public class ComponentRegistry {
* @throws TikaConfigException if the component name is not found
*/
public Class<?> getComponentClass(String name) throws TikaConfigException {
- Class<?> clazz = components.get(name);
- if (clazz == null) {
+ ComponentInfo info = getComponentInfo(name);
+ return info.componentClass();
+ }
+
+ /**
+ * Looks up full component information by name.
+ *
+ * @param name the component name (e.g., "pdf-parser")
+ * @return the component info including class, selfConfiguring flag, and contextKey
+ * @throws TikaConfigException if the component name is not found
+ */
+ public ComponentInfo getComponentInfo(String name) throws TikaConfigException {
+ ComponentInfo info = components.get(name);
+ if (info == null) {
throw new TikaConfigException("Unknown component name: '" + name + "'. " +
"Available components: " + components.keySet());
}
- return clazz;
+ return info;
}
/**
* Returns all registered component names.
*
- * @return unmodifiable map of component names to classes
+ * @return unmodifiable map of component names to component info
*/
- public Map<String, Class<?>> getAllComponents() {
+ public Map<String, ComponentInfo> getAllComponents() {
return Collections.unmodifiableMap(components);
}
@@ -106,9 +134,19 @@ public class ComponentRegistry {
return components.containsKey(name);
}
- private Map<String, Class<?>> loadComponents(String indexFileName)
+ /**
+ * Looks up a component's friendly name by its class.
+ *
+ * @param clazz the component class
+ * @return the friendly name, or null if not registered
+ */
+ public String getFriendlyName(Class<?> clazz) {
+ return classToName.get(clazz);
+ }
+
+ private Map<String, ComponentInfo> loadComponents(String indexFileName)
throws TikaConfigException {
- Map<String, Class<?>> result = new LinkedHashMap<>();
+ Map<String, ComponentInfo> result = new LinkedHashMap<>();
String resourcePath = "META-INF/tika/" + indexFileName + ".idx";
try {
@@ -133,11 +171,12 @@ public class ComponentRegistry {
return result;
}
- private void loadBuiltinAliases(Map<String, Class<?>> result) {
+ private void loadBuiltinAliases(Map<String, ComponentInfo> result) {
for (Map.Entry<String, String> alias : BUILTIN_ALIASES.entrySet()) {
try {
Class<?> clazz = Class.forName(alias.getValue(), false, classLoader);
- result.put(alias.getKey(), clazz);
+ boolean selfConfiguring = SelfConfiguring.class.isAssignableFrom(clazz);
+ result.put(alias.getKey(), new ComponentInfo(clazz, selfConfiguring, null));
} catch (ClassNotFoundException e) {
// External dependency not on classpath - skip this alias
// This is expected behavior, not an error
@@ -145,7 +184,7 @@ public class ComponentRegistry {
}
}
- private void loadFromUrl(URL url, Map<String, Class<?>> result) throws TikaConfigException {
+ private void loadFromUrl(URL url, Map<String, ComponentInfo> result) throws TikaConfigException {
try (InputStream in = url.openStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(in, StandardCharsets.UTF_8))) {
@@ -162,7 +201,7 @@ public class ComponentRegistry {
continue;
}
- // Parse: component-name=fully.qualified.ClassName
+ // Parse: component-name=fully.qualified.ClassName[:key=contextKeyClass]
int equalsIndex = line.indexOf('=');
if (equalsIndex == -1) {
throw new TikaConfigException(
@@ -171,18 +210,49 @@ public class ComponentRegistry {
}
String name = line.substring(0, equalsIndex).trim();
- String className = line.substring(equalsIndex + 1).trim();
+ String value = line.substring(equalsIndex + 1).trim();
- if (name.isEmpty() || className.isEmpty()) {
+ if (name.isEmpty() || value.isEmpty()) {
throw new TikaConfigException(
"Invalid index file format at " + url + " line " + lineNumber +
": name or class is empty");
}
- // Load the class
+ // Parse value: className or className:key=contextKeyClass
+ String className = value;
+ String contextKeyClassName = null;
+
+ int colonIndex = value.indexOf(':');
+ if (colonIndex != -1) {
+ className = value.substring(0, colonIndex);
+ String suffix = value.substring(colonIndex + 1);
+ if (suffix.startsWith("key=")) {
+ contextKeyClassName = suffix.substring(4);
+ } else {
+ throw new TikaConfigException(
+ "Invalid index file format at " + url + " line " + lineNumber +
+ ": unknown suffix '" + suffix + "', expected 'key=...'");
+ }
+ }
+
+ // Load the component class
try {
Class<?> clazz = classLoader.loadClass(className);
- result.put(name, clazz);
+ boolean selfConfiguring = SelfConfiguring.class.isAssignableFrom(clazz);
+
+ // Load the context key class if specified
+ Class<?> contextKey = null;
+ if (contextKeyClassName != null) {
+ try {
+ contextKey = classLoader.loadClass(contextKeyClassName);
+ } catch (ClassNotFoundException e) {
+ throw new TikaConfigException(
+ "Context key class not found: " + contextKeyClassName +
+ " (from " + url + ")", e);
+ }
+ }
+
+ result.put(name, new ComponentInfo(clazz, selfConfiguring, contextKey));
} catch (ClassNotFoundException e) {
throw new TikaConfigException(
"Component class not found: " + className + " (from " + url + ")", e);
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/ConfigLoader.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/ConfigLoader.java
index c9045c9d79..bb9e2b6d30 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/ConfigLoader.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/ConfigLoader.java
@@ -16,6 +16,7 @@
*/
package org.apache.tika.config.loader;
+import java.io.IOException;
import java.util.Set;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -241,14 +242,8 @@ public class ConfigLoader {
}
try {
- // Create a deep copy of defaultValue to avoid mutating the original
- // Using convertValue is efficient and doesn't require serializing to bytes
- @SuppressWarnings("unchecked")
- T copy = objectMapper.convertValue(defaultValue, (Class<T>) defaultValue.getClass());
-
- // Merge JSON properties into the copy
- return objectMapper.readerForUpdating(copy).readValue(node);
- } catch (Exception e) {
+ return JsonMergeUtils.mergeWithDefaults(objectMapper, node, clazz, defaultValue);
+ } catch (IOException e) {
throw new TikaConfigException(
"Failed to merge '" + key + "' into " + clazz.getName(), e);
}
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/JsonMergeUtils.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/JsonMergeUtils.java
new file mode 100644
index 0000000000..be00ccb064
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/JsonMergeUtils.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.config.loader;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Utility methods for merging JSON configurations with default values.
+ * <p>
+ * Provides a consistent pattern for deserializing JSON while preserving
+ * default values for unspecified fields. The original default object is
+ * never modified - a deep copy is created first.
+ */
+public final class JsonMergeUtils {
+
+ private JsonMergeUtils() {
+ // Utility class
+ }
+
+ /**
+ * Deserializes JSON and merges it with a default configuration object.
+ * <p>
+ * Creates a deep copy of the default object, then applies the JSON properties
+ * on top. Fields not specified in the JSON retain their default values.
+ * The original defaultConfig is never modified.
+ *
+ * @param mapper the ObjectMapper to use
+ * @param json the JSON string to deserialize
+ * @param configClass the configuration class
+ * @param defaultConfig the default configuration (will NOT be modified)
+ * @param <T> the configuration type
+ * @return a new object with defaults merged with JSON properties
+ * @throws IOException if deserialization fails
+ */
+ public static <T> T mergeWithDefaults(ObjectMapper mapper, String json,
+ Class<T> configClass, T defaultConfig) throws IOException {
+ if (defaultConfig == null) {
+ return mapper.readValue(json, configClass);
+ }
+
+ // Create a deep copy of defaultConfig to preserve immutability
+ T copy = mapper.convertValue(defaultConfig, configClass);
+
+ // Merge JSON properties into the copy
+ return mapper.readerForUpdating(copy).readValue(json);
+ }
+
+ /**
+ * Deserializes a JsonNode and merges it with a default configuration object.
+ *
+ * @param mapper the ObjectMapper to use
+ * @param node the JsonNode to deserialize
+ * @param configClass the configuration class
+ * @param defaultConfig the default configuration (will NOT be modified)
+ * @param <T> the configuration type
+ * @return a new object with defaults merged with JSON properties
+ * @throws IOException if deserialization fails
+ */
+ public static <T> T mergeWithDefaults(ObjectMapper mapper, JsonNode node,
+ Class<T> configClass, T defaultConfig) throws IOException {
+ if (defaultConfig == null) {
+ return mapper.treeToValue(node, configClass);
+ }
+
+ // Create a deep copy of defaultConfig to preserve immutability
+ @SuppressWarnings("unchecked")
+ T copy = mapper.convertValue(defaultConfig, (Class<T>) defaultConfig.getClass());
+
+ // Merge JSON properties into the copy
+ return mapper.readerForUpdating(copy).readValue(node);
+ }
+
+ /**
+ * Deserializes JSON to a configuration object without merging.
+ *
+ * @param mapper the ObjectMapper to use
+ * @param json the JSON string to deserialize
+ * @param configClass the configuration class
+ * @param <T> the configuration type
+ * @return the deserialized object
+ * @throws IOException if deserialization fails
+ */
+ public static <T> T deserialize(ObjectMapper mapper, String json,
+ Class<T> configClass) throws IOException {
+ return mapper.readValue(json, configClass);
+ }
+}
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 3c13696e47..d7223b3359 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
@@ -22,6 +22,7 @@ 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.PolymorphicObjectMapperFactory;
import org.apache.tika.parser.ParseContext;
@@ -80,20 +81,8 @@ public class ConfigDeserializer {
if (jsonConfig == null) {
return defaultConfig;
}
- String configJson = jsonConfig.json();
- // If there's a default config, merge the user config on top of it
- if (defaultConfig != null) {
- // IMPORTANT: Clone the default config first to preserve immutability
- // Never modify the original defaultConfig as it may be reused across requests
- T configCopy = MAPPER.convertValue(defaultConfig, configClass);
-
- // Now update the copy with user config
- return MAPPER.readerForUpdating(configCopy).readValue(configJson);
- } else {
- // No default config, just deserialize the user config
- return MAPPER.readValue(configJson, configClass);
- }
+ return JsonMergeUtils.mergeWithDefaults(MAPPER, jsonConfig.json(), configClass, defaultConfig);
}
/**
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
index 4cfb95ff72..72b535ead0 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextDeserializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextDeserializer.java
@@ -20,7 +20,6 @@ import static org.apache.tika.serialization.ParseContextSerializer.PARSE_CONTEXT
import java.io.IOException;
import java.util.Iterator;
-import java.util.Map;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
@@ -31,6 +30,20 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.apache.tika.config.ConfigContainer;
import org.apache.tika.parser.ParseContext;
+/**
+ * Deserializes ParseContext from JSON using friendly names.
+ * <p>
+ * All fields are stored in ConfigContainer for later resolution.
+ * Components are resolved at runtime via {@link ParseContextUtils#resolveAll}.
+ * <p>
+ * Example input:
+ * <pre>
+ * {
+ * "pdf-parser": {"extractActions": true},
+ * "tika-task-timeout": {"timeoutMillis": 5000}
+ * }
+ * </pre>
+ */
public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
@Override
@@ -43,11 +56,12 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
/**
* Deserializes a ParseContext from a JsonNode.
- * Uses a properly configured ObjectMapper with polymorphic type handling
- * to ensure objects in the ParseContext are deserialized correctly.
+ * <p>
+ * All fields are stored as JSON in ConfigContainer. Resolution to actual
+ * objects happens later via {@link ParseContextUtils#resolveAll}.
*
* @param jsonNode the JSON node containing the ParseContext data
- * @return the deserialized ParseContext
+ * @return the deserialized ParseContext with ConfigContainer populated
* @throws IOException if deserialization fails
*/
public static ParseContext readParseContext(JsonNode jsonNode) throws IOException {
@@ -61,39 +75,15 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
ParseContext parseContext = new ParseContext();
- // Handle legacy "objects" field - deserialize directly into ParseContext
- if (contextNode.has("objects")) {
- JsonNode objectsNode = contextNode.get("objects");
- for (Map.Entry<String, JsonNode> entry : objectsNode.properties()) {
- String superClassName = entry.getKey();
- JsonNode objectNode = entry.getValue();
-
- try {
- Class<?> superClass = Class.forName(superClassName);
-
- // Let Jackson handle polymorphic deserialization with type info
- // Security is enforced by the PolymorphicTypeValidator in the mapper
- Object deserializedObject = ParseContextSerializer.POLYMORPHIC_MAPPER.treeToValue(objectNode, Object.class);
-
- parseContext.set((Class) superClass, deserializedObject);
- } catch (ClassNotFoundException ex) {
- throw new IOException("Class not found: " + superClassName, ex);
- }
- }
- }
-
- // Store all non-"objects" fields as named configurations in ConfigContainer
- // This allows parsers to look up their config by friendly name (e.g., "pdf-parser")
- // matching the same format used in tika-config.json
+ // 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();
- if (!"objects".equals(fieldName)) {
- if (configContainer == null) {
- configContainer = new ConfigContainer();
- }
- configContainer.set(fieldName, contextNode.get(fieldName).toString());
+ if (configContainer == null) {
+ configContainer = new ConfigContainer();
}
+ configContainer.set(fieldName, contextNode.get(fieldName).toString());
}
if (configContainer != null) {
@@ -102,5 +92,4 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
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
index 998e5c49e7..a4bb139d62 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextSerializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextSerializer.java
@@ -17,7 +17,9 @@
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;
@@ -27,63 +29,95 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.tika.config.ConfigContainer;
+import org.apache.tika.config.loader.ComponentRegistry;
import org.apache.tika.config.loader.PolymorphicObjectMapperFactory;
+import org.apache.tika.exception.TikaConfigException;
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);
+ private static final Logger LOG = LoggerFactory.getLogger(ParseContextSerializer.class);
public static final String PARSE_CONTEXT = "parseContext";
- /**
- * Static ObjectMapper configured for polymorphic serialization/deserialization.
- * Initialized once when the class is loaded to avoid creating a new mapper on each call.
- * Package-private to allow ParseContextDeserializer to use the same mapper.
- */
- static final ObjectMapper POLYMORPHIC_MAPPER = PolymorphicObjectMapperFactory.getMapper();
+ private static final ObjectMapper MAPPER = PolymorphicObjectMapperFactory.getMapper();
+
+ // Lazily loaded registry for looking up friendly names
+ private static volatile ComponentRegistry registry;
+
+ private static ComponentRegistry getRegistry() {
+ if (registry == null) {
+ synchronized (ParseContextSerializer.class) {
+ if (registry == null) {
+ try {
+ registry = new ComponentRegistry("other-configs",
+ ParseContextSerializer.class.getClassLoader());
+ } catch (TikaConfigException e) {
+ LOG.warn("Failed to load component registry for serialization", e);
+ // Return null - objects without friendly names won't be serialized
+ }
+ }
+ }
+ }
+ return registry;
+ }
@Override
public void serialize(ParseContext parseContext, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
- Map<String, Object> contextMap = parseContext.getContextMap();
+ Set<String> writtenKeys = new HashSet<>();
ConfigContainer configContainer = parseContext.get(ConfigContainer.class);
- // Serialize objects stored directly in ParseContext (legacy format)
- // These are objects set via context.set(SomeClass.class, someObject)
- boolean hasNonConfigObjects = contextMap.size() > (configContainer != null ? 1 : 0);
- if (hasNonConfigObjects) {
- jsonGenerator.writeFieldName("objects");
- jsonGenerator.writeStartObject();
+ // 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
+ ComponentRegistry reg = getRegistry();
+ if (reg != null) {
+ Map<String, Object> contextMap = parseContext.getContextMap();
for (Map.Entry<String, Object> entry : contextMap.entrySet()) {
- String className = entry.getKey();
- if (className.equals(ConfigContainer.class.getName())) {
+ // Skip ConfigContainer - already handled above
+ if (entry.getKey().equals(ConfigContainer.class.getName())) {
continue;
}
Object value = entry.getValue();
+ if (value == null) {
+ continue;
+ }
- // Write the field name (superclass/interface name from key)
- jsonGenerator.writeFieldName(className);
-
- // Let Jackson handle type information and serialization
- // Use writerFor(Object.class) to ensure polymorphic type info is added
- POLYMORPHIC_MAPPER.writerFor(Object.class).writeValue(jsonGenerator, value);
- }
-
- jsonGenerator.writeEndObject();
- }
-
- // Write ConfigContainer fields as top-level properties (new friendly-name format)
- // Each field contains a JSON string representing a parser/component configuration
- // using the same friendly names as tika-config.json (e.g., "pdf-parser", "html-parser")
- if (configContainer != null) {
- for (String key : configContainer.getKeys()) {
- jsonGenerator.writeFieldName(key);
- // Write the JSON string as raw JSON (not as a quoted string)
- jsonGenerator.writeRawValue(configContainer.get(key).get().json());
+ // Look up friendly name for this object's class
+ String friendlyName = reg.getFriendlyName(value.getClass());
+ if (friendlyName != null && !writtenKeys.contains(friendlyName)) {
+ jsonGenerator.writeFieldName(friendlyName);
+ MAPPER.writeValue(jsonGenerator, value);
+ writtenKeys.add(friendlyName);
+ }
}
}
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
new file mode 100644
index 0000000000..a69798a3f6
--- /dev/null
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java
@@ -0,0 +1,186 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+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.ComponentRegistry;
+import org.apache.tika.config.loader.PolymorphicObjectMapperFactory;
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.metadata.filter.MetadataFilter;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Utility methods for working with ParseContext objects in JSON-based configurations.
+ * <p>
+ * Supports both legacy verbose format and new friendly-name format:
+ * <pre>
+ * // Legacy format:
+ * "parse-context": {
+ * "objects": {
+ * "org.apache.tika.config.TikaTaskTimeout": {
+ * "@class": "org.apache.tika.config.TikaTaskTimeout",
+ * "timeoutMillis": 30000
+ * }
+ * }
+ * }
+ *
+ * // New friendly-name format:
+ * "parse-context": {
+ * "tika-task-timeout": {
+ * "timeoutMillis": 30000
+ * }
+ * }
+ * </pre>
+ * <p>
+ * Components that implement {@link org.apache.tika.config.SelfConfiguring} are skipped
+ * during resolution - they read their own config from ConfigContainer at runtime.
+ */
+public class ParseContextUtils {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ParseContextUtils.class);
+ private static final ObjectMapper MAPPER = PolymorphicObjectMapperFactory.getMapper();
+
+ /**
+ * Known interfaces that should be used as ParseContext keys.
+ * When a component implements one of these interfaces, the interface is used as
+ * the key in ParseContext instead of the concrete class.
+ * <p>
+ * These are NOT auto-discovered via SPI - they require explicit configuration.
+ */
+ private static final List<Class<?>> KNOWN_CONTEXT_INTERFACES = List.of(
+ MetadataFilter.class
+ // Add other known interfaces as needed
+ );
+
+ /**
+ * Resolves all friendly-named components from ConfigContainer and adds them to ParseContext.
+ * <p>
+ * Iterates through all entries in ConfigContainer, looks up the friendly name in ComponentRegistry,
+ * deserializes the JSON, and adds the instance to ParseContext.
+ * <p>
+ * Components that implement {@link org.apache.tika.config.SelfConfiguring} are skipped -
+ * they read their own config from ConfigContainer at runtime.
+ * <p>
+ * The ParseContext key is determined by:
+ * <ol>
+ * <li>Explicit contextKey from @TikaComponent annotation (if specified)</li>
+ * <li>Auto-detected from {@link #KNOWN_CONTEXT_INTERFACES} (if component implements one)</li>
+ * <li>The component's own class (default)</li>
+ * </ol>
+ *
+ * @param context the ParseContext to populate
+ * @param classLoader the ClassLoader to use for loading component classes
+ */
+ public static void resolveAll(ParseContext context, ClassLoader classLoader) {
+ if (context == null) {
+ return;
+ }
+
+ ConfigContainer container = context.get(ConfigContainer.class);
+ if (container == null) {
+ return;
+ }
+
+ try {
+ // Load the "other-configs" registry which includes parse-context components
+ ComponentRegistry registry = new ComponentRegistry("other-configs", classLoader);
+
+ // Iterate through all configs in the container
+ for (String friendlyName : container.getKeys()) {
+ JsonConfig jsonConfig = container.get(friendlyName, null);
+ if (jsonConfig == null) {
+ continue;
+ }
+
+ ComponentInfo info = null;
+ try {
+ // Try to find this friendly name in the registry
+ info = registry.getComponentInfo(friendlyName);
+
+ // Skip self-configuring components - they handle their own config
+ if (info.selfConfiguring()) {
+ LOG.debug("'{}' is self-configuring, skipping resolution", friendlyName);
+ continue;
+ }
+
+ // Determine the context key
+ Class<?> contextKey = determineContextKey(info, friendlyName);
+
+ // Deserialize and add to ParseContext
+ Object instance = MAPPER.readValue(jsonConfig.json(), info.componentClass());
+ context.set((Class) contextKey, instance);
+
+ LOG.debug("Resolved '{}' -> {} with key {}",
+ friendlyName, info.componentClass().getName(), contextKey.getName());
+ } catch (TikaConfigException e) {
+ // Not a registered component - that's okay, might be used for something else
+ LOG.debug("'{}' not found in other-configs registry, skipping", friendlyName);
+ } catch (IOException e) {
+ LOG.warn("Failed to deserialize component '{}' of type {}", friendlyName,
+ info != null ? info.componentClass().getName() : "unknown", e);
+ }
+ }
+ } catch (TikaConfigException e) {
+ LOG.warn("Failed to load other-configs registry for parse-context resolution", e);
+ }
+ }
+
+ /**
+ * Determines the ParseContext key for a component.
+ *
+ * @param info the component info
+ * @param friendlyName the component's friendly name (for error messages)
+ * @return the class to use as ParseContext key
+ * @throws TikaConfigException if the component implements multiple known interfaces
+ * and no explicit contextKey is specified
+ */
+ private static Class<?> determineContextKey(ComponentInfo info, String friendlyName)
+ throws TikaConfigException {
+ // Use explicit contextKey if provided
+ if (info.contextKey() != null) {
+ return info.contextKey();
+ }
+
+ // Auto-detect from known interfaces
+ List<Class<?>> matches = new ArrayList<>();
+ for (Class<?> iface : KNOWN_CONTEXT_INTERFACES) {
+ if (iface.isAssignableFrom(info.componentClass())) {
+ matches.add(iface);
+ }
+ }
+
+ if (matches.size() > 1) {
+ throw new TikaConfigException(
+ "Component '" + friendlyName + "' (" + info.componentClass().getName() +
+ ") implements multiple known context interfaces: " + matches +
+ ". Use @TikaComponent(contextKey=...) to specify which one to use.");
+ }
+
+ // Use the single matched interface, or fall back to the component class
+ return matches.isEmpty() ? info.componentClass() : matches.get(0);
+ }
+}
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 61f7386233..fdd1ecbff5 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
@@ -17,12 +17,12 @@
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 java.io.StringWriter;
import java.io.Writer;
-import java.util.List;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
@@ -32,12 +32,14 @@ import org.junit.jupiter.api.Test;
import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.loader.PolymorphicObjectMapperFactory;
-import org.apache.tika.extractor.EmbeddedDocumentBytesHandler;
-import org.apache.tika.metadata.filter.CompositeMetadataFilter;
-import org.apache.tika.metadata.filter.DateNormalizingMetadataFilter;
-import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.parser.ParseContext;
+/**
+ * Tests for ParseContext serialization/deserialization.
+ * <p>
+ * All configs use friendly names and are stored in ConfigContainer.
+ * Components are resolved at runtime via ParseContextUtils.resolveAll().
+ */
public class TestParseContextSerialization {
private ObjectMapper createMapper() {
@@ -65,41 +67,24 @@ public class TestParseContextSerialization {
}
}
-
@Test
- public void testBasic() throws Exception {
- MetadataFilter metadataFilter = new CompositeMetadataFilter(List.of(new DateNormalizingMetadataFilter()));
+ public void testEmptyParseContext() throws Exception {
ParseContext pc = new ParseContext();
- pc.set(MetadataFilter.class, metadataFilter);
+ String json = serializeParseContext(pc);
- ConfigContainer configContainer = new ConfigContainer();
- configContainer.set(EmbeddedDocumentBytesHandler.class, """
- {"k1":1,"k2":"val3" }
- """);
- pc.set(ConfigContainer.class, configContainer);
+ // Empty ParseContext should serialize to empty object
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();
- }
+ JsonNode root = mapper.readTree(json);
+ assertEquals(0, root.size(), "Empty ParseContext should have no fields");
+ // Verify round-trip
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
- MetadataFilter dMetadataFilter = deserialized.get(MetadataFilter.class);
- assertTrue(dMetadataFilter instanceof CompositeMetadataFilter);
- List<MetadataFilter> metadataFilters = ((CompositeMetadataFilter) dMetadataFilter).getFilters();
- assertEquals(1, metadataFilters.size());
- assertTrue(metadataFilters.get(0) instanceof DateNormalizingMetadataFilter);
+ assertNotNull(deserialized);
}
@Test
public void testFriendlyNameFormat() throws Exception {
- // Test the new friendly-name format matching tika-config.json
+ // Test the friendly-name format
ParseContext pc = new ParseContext();
ConfigContainer configContainer = new ConfigContainer();
@@ -111,10 +96,11 @@ public class TestParseContextSerialization {
String json = serializeParseContext(pc);
- // Verify JSON structure
+ // Verify JSON structure - should have flat friendly names, no "objects" wrapper
ObjectMapper mapper = createMapper();
JsonNode root = mapper.readTree(json);
+ assertFalse(root.has("objects"), "Should NOT have objects field");
assertTrue(root.has("pdf-parser"), "Should have pdf-parser field");
assertTrue(root.has("html-parser"), "Should have html-parser field");
assertEquals("AUTO", root
@@ -139,58 +125,30 @@ public class TestParseContextSerialization {
}
@Test
- public void testLegacyObjectsFormat() throws Exception {
- // Test the legacy format with "objects" field
- MetadataFilter metadataFilter = new CompositeMetadataFilter(List.of(new DateNormalizingMetadataFilter()));
+ public void testTikaTaskTimeoutFormat() throws Exception {
+ // Test serializing tika-task-timeout configuration
ParseContext pc = new ParseContext();
- pc.set(MetadataFilter.class, metadataFilter);
-
- String json = serializeParseContext(pc);
-
- // Verify JSON has "objects" field
- ObjectMapper mapper = createMapper();
- JsonNode root = mapper.readTree(json);
- assertTrue(root.has("objects"), "Should have objects field for legacy format");
-
- // Verify round-trip
- ParseContext deserialized = mapper.readValue(json, ParseContext.class);
- MetadataFilter deserializedFilter = deserialized.get(MetadataFilter.class);
- assertNotNull(deserializedFilter);
- assertTrue(deserializedFilter instanceof CompositeMetadataFilter);
- }
-
- @Test
- public void testMixedFormat() throws Exception {
- // Test that both legacy objects and new friendly names can coexist
- MetadataFilter metadataFilter = new CompositeMetadataFilter(List.of(new DateNormalizingMetadataFilter()));
- ParseContext pc = new ParseContext();
- pc.set(MetadataFilter.class, metadataFilter);
-
ConfigContainer configContainer = new ConfigContainer();
- configContainer.set("pdf-parser", "{\"ocrStrategy\":\"NO_OCR\"}");
+
+ configContainer.set("tika-task-timeout", "{\"timeoutMillis\":30000}");
pc.set(ConfigContainer.class, configContainer);
String json = serializeParseContext(pc);
- // Verify both formats are present
ObjectMapper mapper = createMapper();
JsonNode root = mapper.readTree(json);
- assertTrue(root.has("objects"), "Should have objects field");
- assertTrue(root.has("pdf-parser"), "Should have pdf-parser field");
+
+ assertTrue(root.has("tika-task-timeout"), "Should have tika-task-timeout field");
+ assertEquals(30000, root
+ .get("tika-task-timeout")
+ .get("timeoutMillis")
+ .asInt());
// Verify round-trip
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
-
- // Check legacy object
- MetadataFilter deserializedFilter = deserialized.get(MetadataFilter.class);
- assertNotNull(deserializedFilter);
- assertTrue(deserializedFilter instanceof CompositeMetadataFilter);
-
- // Check friendly-name config
ConfigContainer deserializedConfig = deserialized.get(ConfigContainer.class);
- assertNotNull(deserializedConfig);
assertTrue(deserializedConfig
- .get("pdf-parser")
+ .get("tika-task-timeout")
.isPresent());
}
@@ -208,10 +166,9 @@ public class TestParseContextSerialization {
// Test hasConfig
assertTrue(ConfigDeserializer.hasConfig(pc, "pdf-parser"));
+ assertFalse(ConfigDeserializer.hasConfig(pc, "non-existent"));
- // Test getConfig with a simple JSON deserialization
- // We can't use actual PDFParserConfig here since we don't have the dependency,
- // but we can verify the JSON is retrieved correctly
+ // Test getConfig retrieves JSON correctly
String retrievedConfig = pc
.get(ConfigContainer.class)
.get("pdf-parser").get().json();
@@ -246,7 +203,7 @@ public class TestParseContextSerialization {
.get("html-parser")
.isPresent());
- // Verify the JSON content
+ // Verify the JSON content is preserved
String pdfParserJson = config
.get("pdf-parser").get().json();
assertTrue(pdfParserJson.contains("AUTO"));
@@ -254,34 +211,72 @@ public class TestParseContextSerialization {
}
@Test
- public void testDeserializeMixedFromJSON() throws Exception {
- // Test deserializing JSON with both legacy objects and friendly names
- // First create the ParseContext and serialize it to get the correct format
- MetadataFilter metadataFilter = new CompositeMetadataFilter(List.of(new DateNormalizingMetadataFilter()));
- ParseContext pc = new ParseContext();
- pc.set(MetadataFilter.class, metadataFilter);
+ public void testDeserializeWithParseContextWrapper() throws Exception {
+ // Test deserializing with optional "parseContext" wrapper
+ String json = """
+ {
+ "parseContext": {
+ "pdf-parser": {
+ "ocrStrategy": "NO_OCR"
+ }
+ }
+ }
+ """;
+
+ ObjectMapper mapper = createMapper();
+ ParseContext deserialized = mapper.readValue(json, ParseContext.class);
+ ConfigContainer config = deserialized.get(ConfigContainer.class);
+ assertNotNull(config);
+ assertTrue(config
+ .get("pdf-parser")
+ .isPresent());
+ }
+
+ @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);
- // Serialize to JSON
+ String json = serializeParseContext(pc);
+
+ // Verify all are present
ObjectMapper mapper = createMapper();
- String json = mapper.writeValueAsString(pc);
+ JsonNode root = mapper.readTree(json);
+
+ assertEquals(4, root.size(), "Should have 4 config fields");
+ assertTrue(root.has("pdf-parser"));
+ assertTrue(root.has("html-parser"));
+ assertTrue(root.has("tika-task-timeout"));
+ assertTrue(root.has("my-custom-config"));
- // Now deserialize it back
+ // Verify round-trip
ParseContext deserialized = mapper.readValue(json, ParseContext.class);
+ ConfigContainer deserializedConfig = deserialized.get(ConfigContainer.class);
+ assertEquals(4, deserializedConfig.getKeys().size());
+ }
- // Verify legacy object was deserialized
- MetadataFilter filter = deserialized.get(MetadataFilter.class);
- assertNotNull(filter);
- assertTrue(filter instanceof CompositeMetadataFilter);
+ @Test
+ public void testProgrammaticObjectsWithoutFriendlyName() throws Exception {
+ // Objects without a registered friendly name are NOT serialized
+ ParseContext pc = new ParseContext();
- // Verify friendly-name config was stored
- ConfigContainer config = deserialized.get(ConfigContainer.class);
- assertNotNull(config);
- assertTrue(config
- .get("pdf-parser")
- .isPresent());
+ // String doesn't have a @TikaComponent annotation, so it won't serialize
+ pc.set(String.class, "test-value");
+
+ String json = serializeParseContext(pc);
+
+ // Should be empty - String doesn't have a friendly name
+ ObjectMapper mapper = createMapper();
+ JsonNode root = mapper.readTree(json);
+ assertEquals(0, root.size(), "Objects without friendly names should not be serialized");
}
}
diff --git a/tika-server/tika-server-core/pom.xml b/tika-server/tika-server-core/pom.xml
index c92a0d0a82..d15fbfce50 100644
--- a/tika-server/tika-server-core/pom.xml
+++ b/tika-server/tika-server-core/pom.xml
@@ -42,6 +42,11 @@
<artifactId>tika-pipes-core</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.tika</groupId>
+ <artifactId>tika-serialization</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>tika-translate</artifactId>
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java
index 0f1a9d005a..59ce60f35c 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java
@@ -22,8 +22,6 @@ import java.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.tika.server.core.config.TimeoutConfig;
-
public class ServerStatusWatcher implements Runnable {
@@ -67,9 +65,10 @@ public class ServerStatusWatcher implements Runnable {
if (millisElapsed > status.timeoutMillis) {
serverStatus.setStatus(ServerStatus.STATUS.TIMEOUT);
LOG.error("Timeout task {}, millis elapsed {}; " +
- "consider increasing the allowable time with the "
- + "<taskTimeoutMillis/> parameter or the {} header",
- status.task.toString(), millisElapsed, TimeoutConfig.X_TIKA_TIMEOUT_MILLIS);
+ "consider increasing the allowable time via: " +
+ "server config (\"taskTimeoutMillis\") or " +
+ "per-request config (\"tika-task-timeout\" in parse-context)",
+ status.task.toString(), millisElapsed);
shutdown();
}
}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java
index 8d2d90eab0..ce048cd6b9 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java
@@ -29,6 +29,7 @@ import jakarta.ws.rs.core.UriInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.tika.config.TikaTaskTimeout;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesResource.java
index 77d807902a..1ffd7d05d8 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesResource.java
@@ -45,6 +45,7 @@ import org.apache.tika.pipes.core.PipesConfig;
import org.apache.tika.pipes.core.PipesException;
import org.apache.tika.pipes.core.PipesParser;
import org.apache.tika.pipes.core.serialization.JsonFetchEmitTuple;
+import org.apache.tika.serialization.ParseContextUtils;
@Path("/pipes")
public class PipesResource {
@@ -91,6 +92,8 @@ public class PipesResource {
try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
t = JsonFetchEmitTuple.fromJson(reader);
}
+ // Resolve friendly-named configs in ParseContext to actual objects
+ ParseContextUtils.resolveAll(t.getParseContext(), getClass().getClassLoader());
return processTuple(t);
}
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 8bdb469e4b..b107c87d2d 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
@@ -27,11 +27,17 @@ import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.sax.SAXTransformerFactory;
@@ -56,6 +62,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.apache.cxf.attachment.ContentDisposition;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,6 +80,8 @@ import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
+import org.apache.tika.serialization.ParseContextDeserializer;
+import org.apache.tika.serialization.ParseContextUtils;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.BodyContentHandler;
import org.apache.tika.sax.ExpandedTitleContentHandler;
@@ -364,27 +373,6 @@ public class TikaResource {
}
}
- protected static long getTaskTimeout(ParseContext parseContext) {
-
- TikaTaskTimeout tikaTaskTimeout = parseContext.get(TikaTaskTimeout.class);
- long timeoutMillis = TIKA_SERVER_CONFIG.getTaskTimeoutMillis();
-
- if (tikaTaskTimeout != null) {
- if (tikaTaskTimeout.getTimeoutMillis() > TIKA_SERVER_CONFIG.getTaskTimeoutMillis()) {
- throw new IllegalArgumentException(
- "Can't request a timeout ( " + tikaTaskTimeout.getTimeoutMillis() + "ms) greater than the taskTimeoutMillis set in the server config (" +
- TIKA_SERVER_CONFIG.getTaskTimeoutMillis() + "ms)");
- }
- timeoutMillis = tikaTaskTimeout.getTimeoutMillis();
- if (timeoutMillis < TIKA_SERVER_CONFIG.getMinimumTimeoutMillis()) {
- throw new WebApplicationException(new IllegalArgumentException(
- "taskTimeoutMillis must be > " + "minimumTimeoutMillis, currently set to (" + TIKA_SERVER_CONFIG.getMinimumTimeoutMillis() + "ms)"),
- Response.Status.BAD_REQUEST);
- }
- }
- return timeoutMillis;
- }
-
public static void checkIsOperating() {
//check that server is not in shutdown mode
if (!SERVER_STATUS.isOperating()) {
@@ -415,6 +403,10 @@ public class TikaResource {
return DEFAULT_HANDLER_CONFIG.isThrowOnWriteLimitReached();
}
+ public static long getTaskTimeout(ParseContext parseContext) {
+ return TikaTaskTimeout.getTimeoutMillis(parseContext, TIKA_SERVER_CONFIG.getTaskTimeoutMillis());
+ }
+
@GET
@Produces("text/plain")
public String getMessage() {
@@ -427,9 +419,53 @@ public class TikaResource {
@Produces("text/plain")
@Path("form")
public StreamingOutput getTextFromMultipart(Attachment att, @Context HttpHeaders httpHeaders, @Context final UriInfo info) throws TikaConfigException, IOException {
+ LOG.info("===== getTextFromMultipart (single Attachment) CALLED =====");
return produceText(att.getObject(InputStream.class), new Metadata(), preparePostHeaderMap(att, httpHeaders), info);
}
+ // Greenfield test endpoint for multipart with config
+ @POST
+ @Consumes("multipart/form-data")
+ @Produces("text/plain")
+ @Path("test-config")
+ public StreamingOutput testMultipartWithConfig(
+ List<Attachment> attachments,
+ @Context HttpHeaders httpHeaders,
+ @Context final UriInfo info) throws TikaConfigException, IOException {
+ LOG.info("===== testMultipartWithConfig CALLED with {} attachments =====", attachments.size());
+
+ // Find the file and config attachments
+ Attachment fileAtt = null;
+ Attachment configAtt = null;
+
+ for (Attachment att : attachments) {
+ ContentDisposition cd = att.getContentDisposition();
+ if (cd != null) {
+ String name = cd.getParameter("name");
+ LOG.info("Found attachment with name: {}", name);
+ if ("file".equals(name)) {
+ fileAtt = att;
+ } else if ("config".equals(name)) {
+ configAtt = att;
+ }
+ }
+ }
+
+ if (fileAtt == null) {
+ throw new IllegalArgumentException("Missing 'file' attachment");
+ }
+ if (configAtt == null) {
+ throw new IllegalArgumentException("Missing 'config' attachment");
+ }
+
+ final Metadata metadata = new Metadata();
+ MultivaluedMap<String, String> headers = preparePostHeaderMap(fileAtt, httpHeaders);
+ return produceTextWithConfig(
+ getInputStream(fileAtt.getObject(InputStream.class), metadata, httpHeaders, info),
+ configAtt.getObject(InputStream.class),
+ metadata, headers, info);
+ }
+
//this is equivalent to text-main in tika-app
@PUT
@Consumes("*/*")
@@ -471,10 +507,63 @@ public class TikaResource {
@Consumes("*/*")
@Produces("text/plain")
public StreamingOutput getText(final InputStream is, @Context HttpHeaders httpHeaders, @Context final UriInfo info) throws TikaConfigException, IOException {
+ LOG.info("===== getText (PUT, no @Path) CALLED =====");
final Metadata metadata = new Metadata();
return produceText(getInputStream(is, metadata, httpHeaders, info), metadata, httpHeaders.getRequestHeaders(), info);
}
+ /**
+ * Produces text output with per-request ParseContext configuration.
+ * Extracts only the parse-context section from the config to allow per-request
+ * configuration of ParseContext objects (e.g., timeout, handler settings).
+ * Uses the server's configured parser to preserve any parser configuration from startup.
+ */
+ private StreamingOutput produceTextWithConfig(final InputStream fileStream, InputStream configStream,
+ final Metadata metadata, MultivaluedMap<String, String> httpHeaders, final UriInfo info)
+ throws TikaConfigException, IOException {
+
+ // Use the server's configured parser (not a new one from the config)
+ final Parser parser = createParser();
+ final ParseContext context = new ParseContext();
+
+ // Read the config JSON to extract only the parse-context section
+ String configJson = new String(configStream.readAllBytes(), StandardCharsets.UTF_8);
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode rootNode = mapper.readTree(configJson);
+
+ JsonNode parseContextNode = rootNode.get("parse-context");
+ LOG.info("found parseContext: " + parseContextNode);
+ if (parseContextNode != null) {
+ // Deserialize parseContext section
+ ParseContext configuredContext = ParseContextDeserializer.readParseContext(parseContextNode);
+
+ // Resolve all friendly-named components from ConfigContainer
+ // For example: "tika-task-timeout" -> TikaTaskTimeout instance
+ ParseContextUtils.resolveAll(configuredContext, Thread.currentThread().getContextClassLoader());
+
+ // Merge configured context into our context
+ for (Map.Entry<String, Object> entry : configuredContext.getContextMap().entrySet()) {
+ try {
+ Class<?> clazz = Class.forName(entry.getKey());
+ context.set((Class) clazz, entry.getValue());
+ } catch (ClassNotFoundException e) {
+ LOG.warn("Could not load class for parseContext entry: " + entry.getKey(), e);
+ }
+ }
+ }
+
+ fillMetadata(parser, metadata, httpHeaders);
+ fillParseContext(httpHeaders, metadata, context);
+
+ logRequest(LOG, "/tika (with config)", metadata);
+
+ return outputStream -> {
+ Writer writer = new OutputStreamWriter(outputStream, UTF_8);
+ BodyContentHandler body = new BodyContentHandler(new RichTextContentHandler(writer));
+ parse(parser, LOG, info.getPath(), fileStream, body, metadata, context);
+ };
+ }
+
public StreamingOutput produceText(final InputStream is, final Metadata metadata, MultivaluedMap<String, String> httpHeaders, final UriInfo info)
throws TikaConfigException, IOException {
final Parser parser = createParser();
@@ -499,6 +588,8 @@ public class TikaResource {
@Produces("text/html")
@Path("form")
public StreamingOutput getHTMLFromMultipart(Attachment att, @Context HttpHeaders httpHeaders, @Context final UriInfo info) throws TikaConfigException, IOException {
+ LOG.info("loading multipart html");
+
return produceOutput(att.getObject(InputStream.class), new Metadata(), preparePostHeaderMap(att, httpHeaders), info, "html");
}
@@ -515,6 +606,8 @@ public class TikaResource {
@Produces("text/xml")
@Path("form")
public StreamingOutput getXMLFromMultipart(Attachment att, @Context HttpHeaders httpHeaders, @Context final UriInfo info) throws TikaConfigException, IOException {
+ LOG.info("loading multipart xml");
+
return produceOutput(att.getObject(InputStream.class), new Metadata(), preparePostHeaderMap(att, httpHeaders), info, "xml");
}
diff --git a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaPipesTest.java b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaPipesTest.java
index 74eec7574f..7bff9149c0 100644
--- a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaPipesTest.java
+++ b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaPipesTest.java
@@ -210,7 +210,6 @@ public class TikaPipesTest extends CXFTestBase {
new EmitKey(EMITTER_JSON_ID, ""), userMetadata, parseContext, FetchEmitTuple.ON_PARSE_EXCEPTION.EMIT);
StringWriter writer = new StringWriter();
JsonFetchEmitTuple.toJson(t, writer);
-
String getUrl = endPoint + PIPES_PATH;
Response response = WebClient
.create(getUrl)
diff --git a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
index 77608bc7d8..50b341eaab 100644
--- a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
+++ b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
@@ -42,6 +42,9 @@ import org.apache.cxf.configuration.security.KeyManagersType;
import org.apache.cxf.configuration.security.KeyStoreType;
import org.apache.cxf.configuration.security.TrustManagersType;
import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.transport.http.HTTPConduit;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -52,7 +55,6 @@ import org.slf4j.LoggerFactory;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.serialization.JsonMetadataList;
-import org.apache.tika.server.core.config.TimeoutConfig;
import org.apache.tika.utils.ProcessUtils;
public class TikaServerIntegrationTest extends IntegrationTestBase {
@@ -120,19 +122,10 @@ public class TikaServerIntegrationTest extends IntegrationTestBase {
testStopped(2000);
}
- @Test
- public void testMinimumTimeoutInHeader() throws Exception {
- startProcess(new String[]{"-config", getConfig("tika-config-server-basic.json")});
- awaitServerStartup();
-
- Response response = WebClient
- .create(endPoint + RMETA_PATH)
- .accept("application/json")
- .header(TimeoutConfig.X_TIKA_TIMEOUT_MILLIS, 1)
- .put(ClassLoader.getSystemResourceAsStream(TEST_HEAVY_HANG));
- assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
- }
-
+ /**
+ * Test legacy header-based timeout configuration.
+ * @deprecated This tests the old header-based approach. Use testTaskTimeoutMultipart instead.
+ */
@Test
public void testTaskTimeoutHeader() throws Exception {
@@ -153,6 +146,41 @@ public class TikaServerIntegrationTest extends IntegrationTestBase {
testStopped(2000);
}
+ /**
+ * Test JSON-based timeout configuration via multipart request.
+ * This is the new recommended approach for configuring timeouts.
+ */
+ @Test
+ public void testTaskTimeoutMultipart() throws Exception {
+ startProcess(new String[]{"-config", getConfig("tika-config-server-basic.json")});
+ awaitServerStartup();
+
+ // Create multipart form with file and config
+ ContentDisposition fileCD = new ContentDisposition("form-data; name=\"file\"; filename=\"heavy_hang_30000.xml\"");
+ ContentDisposition configCD = new ContentDisposition("form-data; name=\"config\"; filename=\"tika-config-timeout-100ms.json\"");
+
+ MultipartBody multipart = new MultipartBody(List.of(
+ new Attachment("file", ClassLoader.getSystemResourceAsStream(TEST_HEAVY_HANG), fileCD),
+ new Attachment("config", ClassLoader.getSystemResourceAsStream("configs/tika-config-timeout-100ms.json"), configCD)
+ ));
+
+ Response response = null;
+ try {
+ LOG.info("TEST: Sending request to: {}", endPoint + "/tika/test-config");
+ response = WebClient
+ .create(endPoint + "/tika/test-config")
+ .accept("text/plain")
+ .type("multipart/form-data")
+ .post(multipart);
+ LOG.info("TEST: Response status: {}", response != null ? response.getStatus() : "null");
+ } catch (Exception e) {
+ LOG.info("TEST: Exception during request", e);
+ //timeout may or may not cause an exception depending on timing
+ }
+ //give some time for the server to crash/terminate itself
+ testStopped(2000);
+ }
+
private void testStopped(long millis) throws InterruptedException {
Thread.sleep(millis);
try {
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 b9b5ae88e2..5ba494c44d 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
@@ -188,6 +188,50 @@ public class TikaServerPipesIntegrationTest extends IntegrationTestBase {
.asText());
}
+ @Test
+ public void testPerRequestTimeout() throws Exception {
+ // Start server with 5000ms timeout (TIKA_CONFIG)
+ // but send a request with 100ms per-request timeout
+ // This should timeout after 100ms, not 5000ms
+ startProcess(new String[]{
+ "-config", ProcessUtils.escapeCommandLine(TIKA_CONFIG
+ .toAbsolutePath()
+ .toString())});
+ JsonNode node = testOneWithPerRequestTimeout("heavy_hang_30000.xml", 100);
+ assertEquals("process_crash", node
+ .get("status")
+ .asText());
+ assertContains("TIMEOUT", node
+ .get("type")
+ .asText());
+ }
+
+ private JsonNode testOneWithPerRequestTimeout(String fileName, long timeoutMillis) throws Exception {
+ awaitServerStartup();
+ Response response = WebClient
+ .create(endPoint + "/pipes")
+ .accept("application/json")
+ .post(getJsonStringWithTimeout(fileName, timeoutMillis));
+ if (response.getStatus() == 200) {
+ Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
+ return new ObjectMapper().readTree(reader);
+ }
+ return null;
+ }
+
+ 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 + "}");
+
+ FetchEmitTuple t = new FetchEmitTuple(fileName,
+ new FetchKey(CXFTestBase.FETCHER_ID, fileName),
+ new EmitKey(CXFTestBase.EMITTER_JSON_ID, ""),
+ new Metadata(),
+ parseContext,
+ FetchEmitTuple.ON_PARSE_EXCEPTION.EMIT);
+ return JsonFetchEmitTuple.toJson(t);
+ }
private JsonNode testOne(String fileName, boolean shouldFileExist) throws Exception {
return testOne(fileName, shouldFileExist, FetchEmitTuple.ON_PARSE_EXCEPTION.EMIT);
diff --git a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.json b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.json
index 5075150c45..7cc6ce5411 100644
--- a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.json
+++ b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.json
@@ -6,7 +6,8 @@
"port": 9999,
"endpoints": [
"rmeta",
- "status"
+ "status",
+ "tika"
]
}
}
(tika) 02/02: TIKA-4562 -- kit and kaboodle
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4562
in repository https://gitbox.apache.org/repos/asf/tika.git
commit a39239c1c1276b063560fc999dfbd86cee9340a4
Author: tallison <ta...@apache.org>
AuthorDate: Wed Dec 10 17:28:52 2025 -0500
TIKA-4562 -- kit and kaboodle
---
.../apache/tika/cli/XmlToJsonConfigConverter.java | 5 +-
.../org/apache/tika/config/ConfigContainer.java | 10 +
.../org/apache/tika/config/ParseContextConfig.java | 23 +-
.../extractor/SkipEmbeddedDocumentSelector.java | 30 +-
.../apache/tika/parser/SimplePasswordProvider.java | 61 ++++
.../apache/tika/parser/pdf/AbstractPDF2XHTML.java | 6 +-
.../java/org/apache/tika/parser/pdf/OCR2XHTML.java | 11 +-
.../java/org/apache/tika/parser/pdf/PDF2XHTML.java | 16 +-
.../tika/parser/pdf/PDFMarkedContent2XHTML.java | 16 +-
.../java/org/apache/tika/parser/pdf/PDFParser.java | 79 ++--
.../apache/tika/parser/pdf/PDFParserConfig.java | 148 +-------
.../org/apache/tika/parser/pdf/PDFParserTest.java | 20 +-
.../apache/tika/parser/pdf/PDFRenderingTest.java | 96 +----
.../org/apache/tika/parser/pdf/PDFParserTest.java | 93 +++++
.../serialization/FetchEmitTupleDeserializer.java | 3 +
.../apache/tika/pipes/core/server/PipesServer.java | 2 +-
.../tika/config/loader/ComponentRegistry.java | 3 +
.../tika/serialization/ConfigDeserializer.java | 11 +-
.../serialization/ParseContextDeserializer.java | 41 ++-
.../tika/serialization/ParseContextSerializer.java | 57 ++-
.../tika/serialization/ParseContextUtils.java | 16 +
.../tika/config/loader/ComponentRegistryTest.java | 2 +-
.../server/core/CompositeParseContextConfig.java | 42 ---
.../tika/server/core/FetcherStreamFactory.java | 2 -
.../tika/server/core/ServerStatusWatcher.java | 2 +-
.../server/core/config/DocumentSelectorConfig.java | 46 ---
.../server/core/config/PasswordProviderConfig.java | 53 ---
.../server/core/resource/DetectorResource.java | 2 -
.../server/core/resource/MetadataResource.java | 35 +-
.../core/resource/RecursiveMetadataResource.java | 56 ++-
.../tika/server/core/resource/TikaResource.java | 371 +++++++------------
.../server/core/resource/UnpackerResource.java | 88 ++++-
.../org.apache.tika.server.core.ParseContextConfig | 17 -
.../server/core/TikaServerIntegrationTest.java | 65 ----
.../server/standard/config/PDFServerConfig.java | 70 ----
.../standard/config/TesseractServerConfig.java | 74 ----
.../org.apache.tika.server.core.ParseContextConfig | 16 -
.../tika/server/standard/MetadataResourceTest.java | 83 +++--
.../standard/RecursiveMetadataResourceTest.java | 54 ++-
.../apache/tika/server/standard/TikaPipesTest.java | 18 +-
.../tika/server/standard/TikaResourceTest.java | 404 ++++++++++++---------
.../tika/server/standard/UnpackerResourceTest.java | 65 +++-
.../standard/UnpackerResourceWithConfigTest.java | 53 ++-
43 files changed, 1096 insertions(+), 1269 deletions(-)
diff --git a/tika-app/src/main/java/org/apache/tika/cli/XmlToJsonConfigConverter.java b/tika-app/src/main/java/org/apache/tika/cli/XmlToJsonConfigConverter.java
index c8d8945fa1..fc43b553d5 100644
--- a/tika-app/src/main/java/org/apache/tika/cli/XmlToJsonConfigConverter.java
+++ b/tika-app/src/main/java/org/apache/tika/cli/XmlToJsonConfigConverter.java
@@ -40,6 +40,7 @@ import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
+import org.apache.tika.config.loader.ComponentInfo;
import org.apache.tika.config.loader.ComponentRegistry;
import org.apache.tika.config.loader.KebabCaseConverter;
import org.apache.tika.exception.TikaConfigException;
@@ -585,8 +586,8 @@ public class XmlToJsonConfigConverter {
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(fullClassName);
// Reverse lookup: find the component name for this class
- for (Map.Entry<String, Class<?>> entry : registry.getAllComponents().entrySet()) {
- if (entry.getValue().equals(clazz)) {
+ for (Map.Entry<String, ComponentInfo> entry : registry.getAllComponents().entrySet()) {
+ if (entry.getValue().componentClass().equals(clazz)) {
return entry.getKey();
}
}
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
index f4eedd5bb2..4ac320833e 100644
--- a/tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java
+++ b/tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java
@@ -86,4 +86,14 @@ public class ConfigContainer implements Serializable {
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 31459cb9f8..888c9e5158 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
@@ -67,12 +67,19 @@ public class ParseContextConfig {
}
/**
- * Retrieves runtime configuration from ParseContext's ConfigContainer.
+ * Retrieves runtime configuration from ParseContext.
+ * <p>
+ * This method first checks if the config is already resolved in ParseContext
+ * (via {@code context.get(configClass)}). If found, it returns immediately without
+ * 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.
* <p>
* This method performs defensive checking: if the ConfigContainer has configuration
* for the requested key but the ConfigDeserializer is not available on the classpath,
- * it throws IllegalStateException. This prevents silent failures where users expect
- * their runtime config to be used but it's silently ignored.
+ * it throws TikaConfigException to prevent silent failures.
*
* @param context the parse context (may be null)
* @param configKey the configuration key (e.g., "pdf-parser", "html-parser")
@@ -90,6 +97,13 @@ public class ParseContextConfig {
return defaultConfig;
}
+ // First check if config is already resolved in ParseContext
+ // (may have been set by a previous call or by user code)
+ T existingConfig = context.get(configClass);
+ if (existingConfig != null) {
+ return existingConfig;
+ }
+
ConfigContainer configContainer = context.get(ConfigContainer.class);
if (configContainer == null) {
return defaultConfig;
@@ -113,6 +127,7 @@ public class ParseContextConfig {
}
// ConfigDeserializer is available - delegate to it
+ // (ConfigDeserializer.getConfig also sets the config in ParseContext for future lookups)
try {
@SuppressWarnings("unchecked")
T result = (T) GET_CONFIG_METHOD.invoke(null, context, configKey, configClass, defaultConfig);
@@ -122,7 +137,7 @@ public class ParseContextConfig {
if (cause instanceof IOException) {
throw (IOException) cause;
}
- throw new IOException("Failed to deserialize config for '" + configKey + "': " +
+ throw new IOException("Failed to deserialize config for '" + configKey + "': " +
cause.getMessage(), cause);
}
}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ParseContextConfig.java b/tika-core/src/main/java/org/apache/tika/extractor/SkipEmbeddedDocumentSelector.java
similarity index 58%
rename from tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ParseContextConfig.java
rename to tika-core/src/main/java/org/apache/tika/extractor/SkipEmbeddedDocumentSelector.java
index 86115dd49f..04bb5b78d5 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ParseContextConfig.java
+++ b/tika-core/src/main/java/org/apache/tika/extractor/SkipEmbeddedDocumentSelector.java
@@ -14,27 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.tika.server.core;
-
-import jakarta.ws.rs.core.MultivaluedMap;
+package org.apache.tika.extractor;
+import org.apache.tika.config.TikaComponent;
import org.apache.tika.metadata.Metadata;
-import org.apache.tika.parser.ParseContext;
/**
- * Implementations must be thread-safe!
- * <p>
- * This class translates http headers into objects/configurations set
- * via the ParseContext
+ * A {@link DocumentSelector} that skips all embedded documents.
+ * When this selector is set on the {@link org.apache.tika.parser.ParseContext},
+ * no embedded documents will be extracted during parsing.
+ *
+ * @since Apache Tika 4.0
*/
-public interface ParseContextConfig {
+@TikaComponent(contextKey = DocumentSelector.class)
+public class SkipEmbeddedDocumentSelector implements DocumentSelector {
- /**
- * Configures the parseContext with present headers.
- *
- * @param headers the headers.
- * @param metadata the metadata.
- * @param context the parse context to configure.
- */
- void configure(MultivaluedMap<String, String> headers, Metadata metadata, ParseContext context);
+ @Override
+ public boolean select(Metadata metadata) {
+ return false;
+ }
}
diff --git a/tika-core/src/main/java/org/apache/tika/parser/SimplePasswordProvider.java b/tika-core/src/main/java/org/apache/tika/parser/SimplePasswordProvider.java
new file mode 100644
index 0000000000..2450a0c281
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/parser/SimplePasswordProvider.java
@@ -0,0 +1,61 @@
+/*
+ * 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 org.apache.tika.config.TikaComponent;
+import org.apache.tika.metadata.Metadata;
+
+/**
+ * A simple {@link PasswordProvider} that returns a configured password
+ * for all documents. This can be configured via JSON in the parseContext:
+ * <pre>
+ * {
+ * "parseContext": {
+ * "simple-password-provider": {
+ * "password": "secret"
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * @since Apache Tika 4.0
+ */
+@TikaComponent(contextKey = PasswordProvider.class)
+public class SimplePasswordProvider implements PasswordProvider {
+
+ private String password;
+
+ public SimplePasswordProvider() {
+ }
+
+ public SimplePasswordProvider(String password) {
+ this.password = password;
+ }
+
+ @Override
+ public String getPassword(Metadata metadata) {
+ return password;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
index fed7c80699..0d950ec0e0 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
@@ -163,6 +163,7 @@ class AbstractPDF2XHTML extends PDFTextStripper {
final EmbeddedDocumentExtractor embeddedDocumentExtractor;
final PDFParserConfig config;
final Parser ocrParser;
+ final Renderer renderer;
/**
* Format used for signature dates
* TODO Make this thread-safe
@@ -199,12 +200,13 @@ class AbstractPDF2XHTML extends PDFTextStripper {
int num3DAnnotations = 0;
AbstractPDF2XHTML(PDDocument pdDocument, ContentHandler handler, ParseContext context,
- Metadata metadata, PDFParserConfig config) throws IOException {
+ Metadata metadata, PDFParserConfig config, Renderer renderer) throws IOException {
this.pdDocument = pdDocument;
this.xhtml = new XHTMLContentHandler(handler, metadata);
this.context = context;
this.metadata = metadata;
this.config = config;
+ this.renderer = renderer;
embeddedDocumentExtractor = EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
if (config.getOcrStrategy() == NO_OCR) {
ocrParser = null;
@@ -592,7 +594,7 @@ class AbstractPDF2XHTML extends PDFTextStripper {
}
}
Metadata pageMetadata = getCurrentPageMetadata(pdPage);
- Renderer thisRenderer = getPDFRenderer(config.getRenderer());
+ Renderer thisRenderer = getPDFRenderer(renderer);
//if there's a configured renderer and if the rendering strategy is "all"
if (thisRenderer != null &&
config.getOcrRenderingStrategy() == PDFParserConfig.OCR_RENDERING_STRATEGY.ALL) {
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/OCR2XHTML.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/OCR2XHTML.java
index 99e8e55268..26a9d33151 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/OCR2XHTML.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/OCR2XHTML.java
@@ -29,6 +29,7 @@ import org.xml.sax.SAXException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.renderer.Renderer;
/**
@@ -38,8 +39,8 @@ import org.apache.tika.parser.ParseContext;
class OCR2XHTML extends AbstractPDF2XHTML {
private OCR2XHTML(PDDocument document, ContentHandler handler, ParseContext context,
- Metadata metadata, PDFParserConfig config) throws IOException {
- super(document, handler, context, metadata, config);
+ Metadata metadata, PDFParserConfig config, Renderer renderer) throws IOException {
+ super(document, handler, context, metadata, config, renderer);
}
/**
@@ -49,17 +50,19 @@ class OCR2XHTML extends AbstractPDF2XHTML {
* @param document PDF document
* @param handler SAX content handler
* @param metadata PDF metadata
+ * @param config PDF parser config
+ * @param renderer the renderer to use for rendering pages
* @throws SAXException if the content handler fails to process SAX events
* @throws TikaException if there was an exception outside of per page processing
*/
public static void process(PDDocument document, ContentHandler handler, ParseContext context,
Metadata metadata,
- PDFParserConfig config)
+ PDFParserConfig config, Renderer renderer)
throws SAXException, TikaException {
OCR2XHTML ocr2XHTML = null;
try {
- ocr2XHTML = new OCR2XHTML(document, handler, context, metadata, config);
+ ocr2XHTML = new OCR2XHTML(document, handler, context, metadata, config, renderer);
ocr2XHTML.writeText(document, new Writer() {
@Override
public void write(char[] cbuf, int off, int len) {
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java
index 15be07454e..3456a87912 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java
@@ -73,8 +73,8 @@ class PDF2XHTML extends AbstractPDF2XHTML {
private AtomicInteger inlineImageCounter = new AtomicInteger(0);
PDF2XHTML(PDDocument document, ContentHandler handler, ParseContext context, Metadata metadata,
- PDFParserConfig config) throws IOException {
- super(document, handler, context, metadata, config);
+ PDFParserConfig config, Renderer renderer) throws IOException {
+ super(document, handler, context, metadata, config, renderer);
}
/**
@@ -84,11 +84,12 @@ class PDF2XHTML extends AbstractPDF2XHTML {
* @param document PDF document
* @param handler SAX content handler
* @param metadata PDF metadata
+ * @param renderer the renderer to use for rendering pages
* @throws SAXException if the content handler fails to process SAX events
* @throws TikaException if there was an exception outside of per page processing
*/
public static void process(PDDocument document, ContentHandler handler, ParseContext context,
- Metadata metadata, PDFParserConfig config)
+ Metadata metadata, PDFParserConfig config, Renderer renderer)
throws SAXException, TikaException {
PDF2XHTML pdf2XHTML = null;
try {
@@ -97,9 +98,9 @@ class PDF2XHTML extends AbstractPDF2XHTML {
// handler.
if (config.isDetectAngles()) {
pdf2XHTML =
- new AngleDetectingPDF2XHTML(document, handler, context, metadata, config);
+ new AngleDetectingPDF2XHTML(document, handler, context, metadata, config, renderer);
} else {
- pdf2XHTML = new PDF2XHTML(document, handler, context, metadata, config);
+ pdf2XHTML = new PDF2XHTML(document, handler, context, metadata, config, renderer);
}
config.configure(pdf2XHTML);
@@ -165,7 +166,6 @@ class PDF2XHTML extends AbstractPDF2XHTML {
//this is the document's inputstream/PDDocument
//TODO: figure out if we can send in the PDPage in the TikaInputStream
TikaInputStream tis = state.getTikaInputStream();
- Renderer renderer = config.getRenderer();
RenderRequest request = new PageRangeRequest(getCurrentPageNo(), getCurrentPageNo());
Metadata renderedMetadata = new Metadata();
renderedMetadata.set(TikaCoreProperties.TYPE, PDFParser.MEDIA_TYPE.toString());
@@ -270,8 +270,8 @@ class PDF2XHTML extends AbstractPDF2XHTML {
private AngleDetectingPDF2XHTML(PDDocument document, ContentHandler handler,
ParseContext context, Metadata metadata,
- PDFParserConfig config) throws IOException {
- super(document, handler, context, metadata, config);
+ PDFParserConfig config, Renderer renderer) throws IOException {
+ super(document, handler, context, metadata, config, renderer);
}
@Override
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFMarkedContent2XHTML.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFMarkedContent2XHTML.java
index e5027c9e31..ca016ac4d9 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFMarkedContent2XHTML.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFMarkedContent2XHTML.java
@@ -47,6 +47,7 @@ import org.xml.sax.SAXException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.renderer.Renderer;
/**
* <p>This was added in Tika 1.24 as an alpha version of a text extractor
@@ -89,9 +90,10 @@ public class PDFMarkedContent2XHTML extends PDF2XHTML {
private State state = new State();
private PDFMarkedContent2XHTML(PDDocument document, ContentHandler handler,
- ParseContext context, Metadata metadata, PDFParserConfig config)
+ ParseContext context, Metadata metadata, PDFParserConfig config,
+ Renderer renderer)
throws IOException {
- super(document, handler, context, metadata, config);
+ super(document, handler, context, metadata, config, renderer);
}
/**
@@ -100,21 +102,23 @@ public class PDFMarkedContent2XHTML extends PDF2XHTML {
*
* @param pdDocument PDF document
* @param handler SAX content handler
- * @param context
+ * @param context parse context
* @param metadata PDF metadata
- * @param config
+ * @param config PDF parser config
+ * @param renderer the renderer to use for rendering pages
* @throws SAXException if the content handler fails to process SAX events
* @throws TikaException if there was an exception outside of per page processing
*/
public static void process(PDDocument pdDocument, ContentHandler handler,
ParseContext context,
- Metadata metadata, PDFParserConfig config)
+ Metadata metadata, PDFParserConfig config, Renderer renderer)
throws SAXException, TikaException {
PDFMarkedContent2XHTML pdfMarkedContent2XHTML = null;
try {
pdfMarkedContent2XHTML =
- new PDFMarkedContent2XHTML(pdDocument, handler, context, metadata, config);
+ new PDFMarkedContent2XHTML(pdDocument, handler, context, metadata, config,
+ renderer);
} catch (IOException e) {
throw new TikaException("couldn't initialize PDFMarkedContent2XHTML", e);
}
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 4a6fb86df9..81e5fa8fe6 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
@@ -58,7 +58,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
-import org.apache.tika.config.ConfigContainer;
import org.apache.tika.config.ConfigDeserializer;
import org.apache.tika.config.Field;
import org.apache.tika.config.Initializable;
@@ -127,7 +126,7 @@ import org.apache.tika.sax.XHTMLContentHandler;
* If your PDFs contain marked content or tags, consider
* {@link PDFParserConfig#setExtractMarkedContent(boolean)}
*/
-@TikaComponent(name = "pdf-parser")
+@TikaComponent
public class PDFParser implements Parser, RenderingParser, Initializable {
public static final MediaType MEDIA_TYPE = MediaType.application("pdf");
@@ -141,6 +140,7 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
private static COSName ENCRYPTED_PAYLOAD = COSName.getPDFName("EncryptedPayload");
private PDFParserConfig defaultConfig = new PDFParserConfig();
+ private Renderer renderer;
public PDFParser() {
}
@@ -234,14 +234,14 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
} else if (localConfig.getOcrStrategy()
.equals(PDFParserConfig.OCR_STRATEGY.OCR_ONLY)) {
OCR2XHTML.process(pdfDocument, handler, context, metadata,
- localConfig);
+ localConfig, renderer);
} else if (hasMarkedContent && localConfig.isExtractMarkedContent()) {
PDFMarkedContent2XHTML
.process(pdfDocument, handler, context, metadata,
- localConfig);
+ localConfig, renderer);
} else {
PDF2XHTML.process(pdfDocument, handler, context, metadata,
- localConfig);
+ localConfig, renderer);
}
}
} catch (InvalidPasswordException e) {
@@ -272,23 +272,16 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
}
private PDFParserConfig getConfig(ParseContext parseContext) throws TikaException, IOException {
- // Check for ConfigContainer with component-specific runtime config
- ConfigContainer configContainer = parseContext.get(ConfigContainer.class);
- if (configContainer != null && configContainer.get("pdf-parser").isPresent()) {
- // Merge runtime config with defaultConfig via serialization
- return ParseContextConfig.getConfig(
- parseContext,
- "pdf-parser",
- PDFParserConfig.class,
- defaultConfig);
- }
-
- // Fall back to old-style ParseContext config for backward compatibility
- PDFParserConfig userConfig = parseContext.get(PDFParserConfig.class);
- if (userConfig != null) {
- return userConfig;
- }
- return defaultConfig;
+ // 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
+ // 3. Set deserialized config in ParseContext for future lookups
+ // 4. Return defaultConfig if no runtime config found
+ return ParseContextConfig.getConfig(
+ parseContext,
+ "pdf-parser",
+ PDFParserConfig.class,
+ defaultConfig);
}
private void checkEncryptedPayload(PDDocument pdfDocument,
@@ -489,7 +482,7 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
throws IOException, TikaException {
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.TYPE, MEDIA_TYPE.toString());
- return localConfig.getRenderer().render(
+ return renderer.render(
tstream, metadata, parseContext, PageRangeRequest.RENDER_ALL);
}
@@ -849,12 +842,12 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
}
@Field
- public void setOcrStrategy(String ocrStrategyString) {
- defaultConfig.setOcrStrategy(ocrStrategyString);
+ public void setOcrStrategy(PDFParserConfig.OCR_STRATEGY ocrStrategy) {
+ defaultConfig.setOcrStrategy(ocrStrategy);
}
- public String getOcrStrategy() {
- return defaultConfig.getOcrStrategy().name();
+ public PDFParserConfig.OCR_STRATEGY getOcrStrategy() {
+ return defaultConfig.getOcrStrategy();
}
@Field
@@ -867,21 +860,21 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
}
@Field
- public void setOcrRenderingStrategy(String ocrRenderingStrategy) {
+ public void setOcrRenderingStrategy(PDFParserConfig.OCR_RENDERING_STRATEGY ocrRenderingStrategy) {
defaultConfig.setOcrRenderingStrategy(ocrRenderingStrategy);
}
- public String getOcrRenderingStrategy() {
- return defaultConfig.getOcrRenderingStrategy().name();
+ public PDFParserConfig.OCR_RENDERING_STRATEGY getOcrRenderingStrategy() {
+ return defaultConfig.getOcrRenderingStrategy();
}
@Field
- public void setOcrImageType(String imageType) {
- defaultConfig.setOcrImageType(imageType);
+ public void setOcrImageType(PDFParserConfig.TikaImageType ocrImageType) {
+ defaultConfig.setOcrImageType(ocrImageType);
}
- public String getOcrImageType() {
- return defaultConfig.getOcrImageType().name();
+ public PDFParserConfig.TikaImageType getOcrImageType() {
+ return defaultConfig.getOcrImageType();
}
@Field
@@ -1146,9 +1139,8 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
}
private void initRenderer(PDFParserConfig config, ParseContext context) {
-
- if (config.getRenderer() != null &&
- config.getRenderer().getSupportedTypes(context).contains(MEDIA_TYPE)) {
+ if (this.renderer != null &&
+ this.renderer.getSupportedTypes(context).contains(MEDIA_TYPE)) {
return;
}
//set a default renderer if nothing was defined
@@ -1156,17 +1148,16 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
pdfBoxRenderer.setDPI(config.getOcrDPI());
pdfBoxRenderer.setImageType(config.getOcrImageType().getImageType());
pdfBoxRenderer.setImageFormatName(config.getOcrImageFormatName());
- config.setRenderer(pdfBoxRenderer);
+ this.renderer = pdfBoxRenderer;
}
- //TODO -- figure out how to deserialize this in TikaConfigSerializer
@Override
public void setRenderer(Renderer renderer) {
- defaultConfig.setRenderer(renderer);
+ this.renderer = renderer;
}
public Renderer getRenderer() {
- return defaultConfig.getRenderer();
+ return renderer;
}
@Field
@@ -1179,12 +1170,12 @@ public class PDFParser implements Parser, RenderingParser, Initializable {
}
@Field
- public void setImageStrategy(String imageStrategy) {
+ public void setImageStrategy(PDFParserConfig.IMAGE_STRATEGY imageStrategy) {
defaultConfig.setImageStrategy(imageStrategy);
}
- public String getImageStrategy() {
- return defaultConfig.getImageStrategy().name();
+ public PDFParserConfig.IMAGE_STRATEGY getImageStrategy() {
+ return defaultConfig.getImageStrategy();
}
/**
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java
index 17a743200a..fa64ad9161 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java
@@ -25,7 +25,6 @@ import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.tika.parser.pdf.image.ImageGraphicsEngineFactory;
-import org.apache.tika.renderer.Renderer;
/**
* Config for PDFParser.
@@ -149,8 +148,6 @@ public class PDFParserConfig implements Serializable {
private boolean detectAngles = false;
- private Renderer renderer;
-
private boolean extractIncrementalUpdateInfo = true;
private boolean parseIncrementalUpdates = false;
@@ -590,23 +587,10 @@ public class PDFParserConfig implements Serializable {
}
}
- /**
- * Which strategy to use for OCR
- *
- * @param ocrStrategyString
- */
- public void setOcrStrategy(String ocrStrategyString) {
- setOcrStrategy(OCR_STRATEGY.parse(ocrStrategyString));
- }
-
public OCR_RENDERING_STRATEGY getOcrRenderingStrategy() {
return ocrRenderingStrategy;
}
- public void setOcrRenderingStrategy(String ocrRenderingStrategyString) {
- setOcrRenderingStrategy(OCR_RENDERING_STRATEGY.parse(ocrRenderingStrategyString));
- }
-
/**
* When rendering the page for OCR, do you want to include the rendering of the electronic text,
* ALL, or do you only want to run OCR on the images and vector graphics (NO_TEXT)?
@@ -661,15 +645,6 @@ public class PDFParserConfig implements Serializable {
this.ocrImageType = ocrImageType;
}
- /**
- * Image type used to render the page image for OCR.
- *
- * @see #setOcrImageType(TikaImageType)
- */
- public void setOcrImageType(String ocrImageTypeString) {
- setOcrImageType(parseImageType(ocrImageTypeString));
- }
-
/**
* Dots per inch used to render the page image for OCR
*
@@ -766,26 +741,6 @@ public class PDFParserConfig implements Serializable {
this.setKCMS = setKCMS;
}
- private TikaImageType parseImageType(String ocrImageType) {
- for (TikaImageType t : TikaImageType.values()) {
- if (ocrImageType.equalsIgnoreCase(t.toString())) {
- return t;
- }
- }
- StringBuilder sb = new StringBuilder();
- sb.append("I regret that I could not parse '");
- sb.append(ocrImageType);
- sb.append("'. I'm only familiar with: ");
- int i = 0;
- for (ImageType t : ImageType.values()) {
- if (i++ == 0) {
- sb.append(", ");
- }
- sb.append(t.toString());
- }
- throw new IllegalArgumentException(sb.toString());
- }
-
public boolean isDetectAngles() {
return detectAngles;
}
@@ -794,18 +749,6 @@ public class PDFParserConfig implements Serializable {
this.detectAngles = detectAngles;
}
- public void setRenderer(Renderer renderer) {
- this.renderer = renderer;
- }
-
- public Renderer getRenderer() {
- return renderer;
- }
-
- public void setImageStrategy(String imageStrategy) {
- setImageStrategy(PDFParserConfig.IMAGE_STRATEGY.parse(imageStrategy));
- }
-
public void setImageStrategy(IMAGE_STRATEGY imageStrategy) {
this.imageStrategy = imageStrategy;
}
@@ -866,33 +809,7 @@ public class PDFParserConfig implements Serializable {
}
public enum OCR_STRATEGY {
- AUTO, NO_OCR, OCR_ONLY, OCR_AND_TEXT_EXTRACTION;
-
- private static OCR_STRATEGY parse(String s) {
- if (s == null) {
- return NO_OCR;
- } else if ("no_ocr".equals(s.toLowerCase(Locale.ROOT))) {
- return NO_OCR;
- } else if ("ocr_only".equals(s.toLowerCase(Locale.ROOT))) {
- return OCR_ONLY;
- } else if (s.toLowerCase(Locale.ROOT).contains("ocr_and_text")) {
- return OCR_AND_TEXT_EXTRACTION;
- } else if ("auto".equals(s.toLowerCase(Locale.ROOT))) {
- return AUTO;
- }
- StringBuilder sb = new StringBuilder();
- sb.append("I regret that I don't recognize '").append(s);
- sb.append("' as an OCR_STRATEGY. I only recognize:");
- int i = 0;
- for (OCR_STRATEGY strategy : OCR_STRATEGY.values()) {
- if (i++ > 0) {
- sb.append(", ");
- }
- sb.append(strategy.toString());
-
- }
- throw new IllegalArgumentException(sb.toString());
- }
+ AUTO, NO_OCR, OCR_ONLY, OCR_AND_TEXT_EXTRACTION
}
/**
@@ -956,42 +873,11 @@ public class PDFParserConfig implements Serializable {
}
public enum OCR_RENDERING_STRATEGY {
-
NO_TEXT, //includes vector graphics and image
TEXT_ONLY, //renders only glyphs
VECTOR_GRAPHICS_ONLY, //renders only vector graphics
- ALL;
+ ALL
//TODO: add AUTO?
-
- private static OCR_RENDERING_STRATEGY parse(String s) {
- if (s == null) {
- return ALL;
- }
- String lc = s.toLowerCase(Locale.US);
- switch (lc) {
- case "vector_graphics_only":
- return VECTOR_GRAPHICS_ONLY;
- case "text_only":
- return TEXT_ONLY;
- case "no_text":
- return NO_TEXT;
- case "all":
- return ALL;
- }
-
- StringBuilder sb = new StringBuilder();
- sb.append("I regret that I don't recognize '").append(s);
- sb.append("' as an OCR_STRATEGY. I only recognize:");
- int i = 0;
- for (OCR_RENDERING_STRATEGY strategy : OCR_RENDERING_STRATEGY.values()) {
- if (i++ > 0) {
- sb.append(", ");
- }
- sb.append(strategy.toString());
-
- }
- throw new IllegalArgumentException(sb.toString());
- }
}
public enum IMAGE_STRATEGY {
@@ -1012,35 +898,7 @@ public class PDFParserConfig implements Serializable {
* For some rendering engines, this may be slower, but it allows the writing
* of image metadata into the xhtml in the proper location
*/
- RENDER_PAGES_AT_PAGE_END;
+ RENDER_PAGES_AT_PAGE_END
//TODO: add LOGICAL_IMAGES
-
- private static IMAGE_STRATEGY parse(String s) {
- String lc = s.toLowerCase(Locale.US);
- switch (lc) {
- case "rawimages" :
- return RAW_IMAGES;
- case "renderpagesbeforeparse":
- return RENDER_PAGES_BEFORE_PARSE;
- case "renderpagesatpageend":
- return RENDER_PAGES_AT_PAGE_END;
- case "none":
- return NONE;
- default:
- //fall through to exception
- break;
- }
- StringBuilder sb = new StringBuilder();
- sb.append("I regret that I don't recognize '").append(s);
- sb.append("' as an IMAGE_STRATEGY. I only recognize:");
- int i = 0;
- for (IMAGE_STRATEGY strategy : IMAGE_STRATEGY.values()) {
- if (i++ > 0) {
- sb.append(", ");
- }
- sb.append(strategy.toString());
- }
- throw new IllegalArgumentException(sb.toString());
- }
}
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
index 1ae9242ef4..b4645386db 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
@@ -1142,25 +1142,7 @@ public class PDFParserTest extends TikaTest {
}
}
- @Test
- public void testInitializationOfNonPrimitivesViaConfig() throws Exception {
- try (InputStream is = getResourceAsStream(
- "/org/apache/tika/parser/pdf/tika-config-non-primitives.xml")) {
- assertNotNull(is);
- TikaConfig tikaConfig = new TikaConfig(is);
- AutoDetectParser p = new AutoDetectParser(tikaConfig);
- Map<MediaType, Parser> parsers = p.getParsers();
- Parser composite = parsers.get(MediaType.application("pdf"));
- Parser pdfParser =
- ((CompositeParser) composite).getParsers().get(MediaType.application("pdf"));
- assertEquals("org.apache.tika.parser.pdf.PDFParser",
- pdfParser.getClass().getName());
- assertEquals(PDFParserConfig.OCR_STRATEGY.OCR_ONLY,
- ((PDFParser) pdfParser).getPDFParserConfig().getOcrStrategy());
- assertEquals(PDFParserConfig.TikaImageType.GRAY.RGB,
- ((PDFParser) pdfParser).getPDFParserConfig().getOcrImageType());
- }
- }
+ // Moved to tika-parsers-standard-package PDFParserTest.testInitializationOfNonPrimitivesViaJsonConfig
@Test
public void testDiffTitles() throws Exception {
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFRenderingTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFRenderingTest.java
index d7055e297e..0bed4dc75f 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFRenderingTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFRenderingTest.java
@@ -17,31 +17,13 @@
package org.apache.tika.parser.pdf;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import org.junit.jupiter.api.Test;
-import org.xml.sax.ContentHandler;
-import org.xml.sax.SAXException;
import org.apache.tika.TikaTest;
-import org.apache.tika.config.TikaConfig;
-import org.apache.tika.exception.TikaException;
-import org.apache.tika.extractor.EmbeddedDocumentExtractor;
-import org.apache.tika.extractor.ParsingEmbeddedDocumentExtractor;
-import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
-import org.apache.tika.metadata.TikaCoreProperties;
-import org.apache.tika.metadata.TikaPagedText;
-import org.apache.tika.parser.AutoDetectParser;
-import org.apache.tika.parser.ParseContext;
-import org.apache.tika.parser.Parser;
public class PDFRenderingTest extends TikaTest {
@@ -51,80 +33,6 @@ public class PDFRenderingTest extends TikaTest {
assertEquals(1, metadataList.size());
}
- @Test
- public void testBasic() throws Exception {
- ParseContext parseContext = configureParseContext();
- TikaConfig config = getConfig("tika-rendering-config.xml");
- Parser p = new AutoDetectParser(config);
- List<Metadata> metadataList = getRecursiveMetadata("testPDF.pdf", p, parseContext);
- Map<Integer, byte[]> embedded =
- ((RenderCaptureExtractor)parseContext.get(EmbeddedDocumentExtractor.class))
- .getEmbedded();
- assertEquals(1, embedded.size());
- assertTrue(embedded.containsKey(0));
- //what else can we do to test this? File type == tiff? Run OCR?
- assertTrue(embedded.get(0).length > 1000);
-
- assertEquals(2, metadataList.size());
- Metadata tiffMetadata = metadataList.get(1);
- assertEquals("RENDERING", tiffMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
- assertEquals(1, tiffMetadata.getInt(TikaPagedText.PAGE_NUMBER));
- }
-
- @Test
- public void testRotated() throws Exception {
- ParseContext parseContext = configureParseContext();
- TikaConfig config = getConfig("tika-rendering-config.xml");
- Parser p = new AutoDetectParser(config);
- List<Metadata> metadataList = getRecursiveMetadata("testPDF_rotated.pdf", p, parseContext);
- Map<Integer, byte[]> embedded =
- ((RenderCaptureExtractor)parseContext.get(EmbeddedDocumentExtractor.class))
- .getEmbedded();
-
- assertEquals(1, embedded.size());
- assertTrue(embedded.containsKey(0));
- //what else can we do to test this? File type == tiff? Run OCR?
- assertTrue(embedded.get(0).length > 1000);
-
- assertEquals(2, metadataList.size());
- Metadata tiffMetadata = metadataList.get(1);
- assertEquals("RENDERING", tiffMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
- assertEquals(1, tiffMetadata.getInt(TikaPagedText.PAGE_NUMBER));
- assertEquals(90.0, Double.parseDouble(tiffMetadata.get(TikaPagedText.PAGE_ROTATION)), 0.1);
- }
-
- private TikaConfig getConfig(String path) throws TikaException, IOException, SAXException {
- try (InputStream is = PDFRenderingTest.class.getResourceAsStream(path)) {
- return new TikaConfig(is);
- }
- }
-
- private ParseContext configureParseContext() {
- ParseContext parseContext = new ParseContext();
- parseContext.set(EmbeddedDocumentExtractor.class, new RenderCaptureExtractor(parseContext));
- return parseContext;
- }
-
-
- private class RenderCaptureExtractor extends ParsingEmbeddedDocumentExtractor {
- private int count = 0;
- Map<Integer, byte[]> embedded = new HashMap<>();
-
- public RenderCaptureExtractor(ParseContext context) {
- super(context);
- }
-
- @Override
- public void parseEmbedded(TikaInputStream tis, ContentHandler handler, Metadata metadata,
- boolean outputHtml) throws SAXException, IOException {
-
- byte[] bytes = Files.readAllBytes(tis.getPath());
- embedded.put(count++, bytes);
- super.parseEmbedded(tis, handler, metadata, outputHtml);
- }
-
- public Map<Integer, byte[]> getEmbedded() {
- return embedded;
- }
- }
+ // testBasic and testRotated moved to tika-parsers-standard-package
+ // PDFParserTest.testRenderingBasic and PDFParserTest.testRenderingRotated
}
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 064741211f..d0aa70519e 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
@@ -24,9 +24,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -37,17 +41,22 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
import org.apache.tika.TikaLoaderHelper;
import org.apache.tika.TikaTest;
import org.apache.tika.config.loader.PolymorphicObjectMapperFactory;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.extractor.ContainerExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.ParserContainerExtractor;
+import org.apache.tika.extractor.ParsingEmbeddedDocumentExtractor;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.PDF;
import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.metadata.TikaPagedText;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
@@ -601,4 +610,88 @@ public class PDFParserTest extends TikaTest {
assertEquals(PDFParserConfig.OCR_STRATEGY.AUTO, deserializedConfig.getOcrStrategy(),
"ocrStrategy should be preserved");
}
+
+ @Test
+ public void testRenderingBasic() throws Exception {
+ ParseContext parseContext = configureRenderingParseContext();
+ Parser p = TikaLoaderHelper.getLoader("tika-config-rendering.json").loadAutoDetectParser();
+ List<Metadata> metadataList = getRecursiveMetadata("testPDF.pdf", p, parseContext);
+ Map<Integer, byte[]> embedded =
+ ((RenderCaptureExtractor) parseContext.get(EmbeddedDocumentExtractor.class))
+ .getEmbedded();
+ assertEquals(1, embedded.size());
+ assertTrue(embedded.containsKey(0));
+ assertTrue(embedded.get(0).length > 1000);
+
+ assertEquals(2, metadataList.size());
+ Metadata tiffMetadata = metadataList.get(1);
+ assertEquals("RENDERING", tiffMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ assertEquals(1, tiffMetadata.getInt(TikaPagedText.PAGE_NUMBER));
+ }
+
+ @Test
+ public void testRenderingRotated() throws Exception {
+ ParseContext parseContext = configureRenderingParseContext();
+ Parser p = TikaLoaderHelper.getLoader("tika-config-rendering.json").loadAutoDetectParser();
+ List<Metadata> metadataList = getRecursiveMetadata("testPDF_rotated.pdf", p, parseContext);
+ Map<Integer, byte[]> embedded =
+ ((RenderCaptureExtractor) parseContext.get(EmbeddedDocumentExtractor.class))
+ .getEmbedded();
+
+ assertEquals(1, embedded.size());
+ assertTrue(embedded.containsKey(0));
+ assertTrue(embedded.get(0).length > 1000);
+
+ assertEquals(2, metadataList.size());
+ Metadata tiffMetadata = metadataList.get(1);
+ assertEquals("RENDERING", tiffMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ assertEquals(1, tiffMetadata.getInt(TikaPagedText.PAGE_NUMBER));
+ assertEquals(90.0, Double.parseDouble(tiffMetadata.get(TikaPagedText.PAGE_ROTATION)), 0.1);
+ }
+
+ @Test
+ public void testInitializationOfNonPrimitivesViaJsonConfig() throws Exception {
+ Parser p = TikaLoaderHelper.getLoader("tika-config-non-primitives.json").loadAutoDetectParser();
+ assertTrue(p instanceof org.apache.tika.parser.AutoDetectParser);
+ org.apache.tika.parser.AutoDetectParser adp = (org.apache.tika.parser.AutoDetectParser) p;
+ java.util.Map<MediaType, Parser> parsers = adp.getParsers();
+ Parser composite = parsers.get(MediaType.application("pdf"));
+ Parser pdfParser =
+ ((org.apache.tika.parser.CompositeParser) composite).getParsers()
+ .get(MediaType.application("pdf"));
+ assertEquals("org.apache.tika.parser.pdf.PDFParser",
+ pdfParser.getClass().getName());
+ assertEquals(PDFParserConfig.OCR_STRATEGY.OCR_ONLY,
+ ((PDFParser) pdfParser).getPDFParserConfig().getOcrStrategy());
+ assertEquals(PDFParserConfig.TikaImageType.RGB,
+ ((PDFParser) pdfParser).getPDFParserConfig().getOcrImageType());
+ }
+
+ private ParseContext configureRenderingParseContext() {
+ ParseContext parseContext = new ParseContext();
+ parseContext.set(EmbeddedDocumentExtractor.class, new RenderCaptureExtractor(parseContext));
+ return parseContext;
+ }
+
+ private static class RenderCaptureExtractor extends ParsingEmbeddedDocumentExtractor {
+ private int count = 0;
+ Map<Integer, byte[]> embedded = new HashMap<>();
+
+ public RenderCaptureExtractor(ParseContext context) {
+ super(context);
+ }
+
+ @Override
+ public void parseEmbedded(TikaInputStream tis, ContentHandler handler, Metadata metadata,
+ boolean outputHtml) throws SAXException, IOException {
+
+ byte[] bytes = Files.readAllBytes(tis.getPath());
+ embedded.put(count++, bytes);
+ super.parseEmbedded(tis, handler, metadata, outputHtml);
+ }
+
+ public Map<Integer, byte[]> getEmbedded() {
+ return embedded;
+ }
+ }
}
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 018a188939..767e4ef3c2 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
@@ -43,6 +43,7 @@ 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;
public class FetchEmitTupleDeserializer extends JsonDeserializer<FetchEmitTuple> {
@@ -60,6 +61,8 @@ 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
+ ParseContextUtils.resolveAll(parseContext, FetchEmitTupleDeserializer.class.getClassLoader());
FetchEmitTuple.ON_PARSE_EXCEPTION onParseException = readOnParseException(root);
return new FetchEmitTuple(id, new FetchKey(fetcherId, fetchKey, fetchRangeStart, fetchRangeEnd),
diff --git a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
index c971367ab5..974afad089 100644
--- a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
+++ b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
@@ -58,7 +58,6 @@ import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.detect.Detector;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
-import org.apache.tika.serialization.ParseContextUtils;
import org.apache.tika.extractor.RUnpackExtractorFactory;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.filter.MetadataFilter;
@@ -74,6 +73,7 @@ import org.apache.tika.pipes.core.PipesConfig;
import org.apache.tika.pipes.core.emitter.EmitterManager;
import org.apache.tika.pipes.core.fetcher.FetcherManager;
import org.apache.tika.plugins.TikaPluginManager;
+import org.apache.tika.serialization.ParseContextUtils;
import org.apache.tika.utils.ExceptionUtils;
/**
diff --git a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java
index 2978f09513..1d26438f5c 100644
--- a/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java
+++ b/tika-serialization/src/main/java/org/apache/tika/config/loader/ComponentRegistry.java
@@ -61,6 +61,9 @@ public class ComponentRegistry {
aliases.put("ogg-detector", "org.gagravarr.tika.OggDetector");
// HandlerConfig is in tika-pipes-api which can't depend on tika-core for @TikaComponent
aliases.put("handler-config", "org.apache.tika.pipes.api.HandlerConfig");
+ // EmbeddedDocumentBytesConfig is in tika-pipes-core which can't depend on tika-core for @TikaComponent
+ aliases.put("embedded-document-bytes-config",
+ "org.apache.tika.pipes.core.extractor.EmbeddedDocumentBytesConfig");
return Collections.unmodifiableMap(aliases);
}
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 d7223b3359..c8feb39919 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
@@ -57,6 +57,9 @@ public class ConfigDeserializer {
/**
* 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.
+ * <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)}.
*
* @param context the parse context containing the ConfigContainer
* @param configKey the configuration key (e.g., "pdf-parser", "html-parser")
@@ -82,7 +85,13 @@ public class ConfigDeserializer {
return defaultConfig;
}
- return JsonMergeUtils.mergeWithDefaults(MAPPER, jsonConfig.json(), configClass, defaultConfig);
+ 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)
+ context.set(configClass, config);
+
+ return config;
}
/**
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
index 72b535ead0..011f149cc5 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextDeserializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextDeserializer.java
@@ -26,26 +26,34 @@ 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.loader.PolymorphicObjectMapperFactory;
import org.apache.tika.parser.ParseContext;
/**
* Deserializes ParseContext from JSON using friendly names.
* <p>
- * All fields are stored in ConfigContainer for later resolution.
- * Components are resolved at runtime via {@link ParseContextUtils#resolveAll}.
+ * 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}
+ * "tika-task-timeout": {"timeoutMillis": 5000},
+ * "org.apache.tika.metadata.filter.MetadataFilter": {"@class": "...", ...}
* }
* </pre>
*/
public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
+ private static final Logger LOG = LoggerFactory.getLogger(ParseContextDeserializer.class);
+ private static final ObjectMapper MAPPER = PolymorphicObjectMapperFactory.getMapper();
+
@Override
public ParseContext deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext)
@@ -57,8 +65,11 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
/**
* Deserializes a ParseContext from a JsonNode.
* <p>
- * All fields are stored as JSON in ConfigContainer. Resolution to actual
- * objects happens later via {@link ParseContextUtils#resolveAll}.
+ * 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
@@ -80,10 +91,28 @@ public class ParseContextDeserializer extends JsonDeserializer<ParseContext> {
ConfigContainer configContainer = null;
for (Iterator<String> it = contextNode.fieldNames(); it.hasNext(); ) {
String fieldName = it.next();
+ JsonNode fieldValue = contextNode.get(fieldName);
+
+ // Check if fieldName is a full class name (for directly serialized Tika types)
+ if (fieldName.startsWith("org.apache.tika.")) {
+ try {
+ Class<?> keyClass = Class.forName(fieldName);
+ // Deserialize using the key class as the target type
+ Object value = MAPPER.treeToValue(fieldValue, keyClass);
+ parseContext.set((Class) keyClass, value);
+ continue;
+ } catch (ClassNotFoundException e) {
+ LOG.debug("Class not found for key '{}', storing in ConfigContainer", fieldName);
+ } catch (Exception e) {
+ LOG.warn("Failed to deserialize '{}' directly, storing in ConfigContainer", fieldName, e);
+ }
+ }
+
+ // Store as config for later resolution
if (configContainer == null) {
configContainer = new ConfigContainer();
}
- configContainer.set(fieldName, contextNode.get(fieldName).toString());
+ configContainer.set(fieldName, fieldValue.toString());
}
if (configContainer != null) {
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
index a4bb139d62..a8df312dc6 100644
--- a/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextSerializer.java
+++ b/tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextSerializer.java
@@ -97,27 +97,52 @@ public class ParseContextSerializer extends JsonSerializer<ParseContext> {
}
// Then, serialize objects from ParseContext that have registered friendly names
+ // or are stored under Tika type keys (for polymorphic custom subclasses)
ComponentRegistry reg = getRegistry();
- if (reg != null) {
- 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;
- }
+ 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;
- }
+ Object value = entry.getValue();
+ if (value == null) {
+ continue;
+ }
+
+ // Try to get friendly name for this object's class
+ String friendlyName = (reg != null) ? reg.getFriendlyName(value.getClass()) : null;
- // Look up friendly name for this object's class
- String friendlyName = reg.getFriendlyName(value.getClass());
- if (friendlyName != null && !writtenKeys.contains(friendlyName)) {
- jsonGenerator.writeFieldName(friendlyName);
+ String key;
+ if (friendlyName != null) {
+ // Use friendly name if available
+ key = friendlyName;
+ } else if (entry.getKey().startsWith("org.apache.tika.")) {
+ // For Tika types without friendly names (e.g., custom MetadataFilter subclasses),
+ // use the context key - polymorphic mapper will add @class for the concrete type
+ 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 using the context key (not friendly name), we need to serialize
+ // with the base type to get polymorphic @class info for custom subclasses
+ if (friendlyName == null) {
+ try {
+ Class<?> contextKeyClass = Class.forName(entry.getKey());
+ MAPPER.writerFor(contextKeyClass).writeValue(jsonGenerator, value);
+ } catch (ClassNotFoundException e) {
+ // Fallback to default serialization
+ MAPPER.writeValue(jsonGenerator, value);
+ }
+ } else {
MAPPER.writeValue(jsonGenerator, value);
- writtenKeys.add(friendlyName);
}
+ writtenKeys.add(key);
}
}
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 a69798a3f6..d06b63f58e 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
@@ -91,6 +91,9 @@ 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
@@ -105,6 +108,8 @@ public class ParseContextUtils {
return;
}
+ List<String> resolvedKeys = new ArrayList<>();
+
try {
// Load the "other-configs" registry which includes parse-context components
ComponentRegistry registry = new ComponentRegistry("other-configs", classLoader);
@@ -133,6 +138,7 @@ public class ParseContextUtils {
// Deserialize and add to ParseContext
Object instance = MAPPER.readValue(jsonConfig.json(), info.componentClass());
context.set((Class) contextKey, instance);
+ resolvedKeys.add(friendlyName);
LOG.debug("Resolved '{}' -> {} with key {}",
friendlyName, info.componentClass().getName(), contextKey.getName());
@@ -147,6 +153,16 @@ public class ParseContextUtils {
} catch (TikaConfigException e) {
LOG.warn("Failed to load other-configs registry for parse-context resolution", e);
}
+
+ // 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);
+ }
}
/**
diff --git a/tika-serialization/src/test/java/org/apache/tika/config/loader/ComponentRegistryTest.java b/tika-serialization/src/test/java/org/apache/tika/config/loader/ComponentRegistryTest.java
index 377fa81b64..cf4bdd2809 100644
--- a/tika-serialization/src/test/java/org/apache/tika/config/loader/ComponentRegistryTest.java
+++ b/tika-serialization/src/test/java/org/apache/tika/config/loader/ComponentRegistryTest.java
@@ -66,7 +66,7 @@ public class ComponentRegistryTest {
ComponentRegistry registry = new ComponentRegistry("parsers",
getClass().getClassLoader());
- Map<String, Class<?>> all = registry.getAllComponents();
+ Map<String, ComponentInfo> all = registry.getAllComponents();
assertNotNull(all, "All components map should not be null");
assertTrue(all.size() >= 4, "Should have at least 4 test parsers");
}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/CompositeParseContextConfig.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/CompositeParseContextConfig.java
deleted file mode 100644
index 29ab876b12..0000000000
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/CompositeParseContextConfig.java
+++ /dev/null
@@ -1,42 +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.server.core;
-
-import java.util.List;
-
-import jakarta.ws.rs.core.MultivaluedMap;
-
-import org.apache.tika.config.ServiceLoader;
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.parser.ParseContext;
-
-public class CompositeParseContextConfig implements ParseContextConfig {
-
-
- final List<ParseContextConfig> configs;
-
- public CompositeParseContextConfig() {
- configs = new ServiceLoader(CompositeParseContextConfig.class.getClassLoader()).loadServiceProviders(ParseContextConfig.class);
- }
-
- @Override
- public void configure(MultivaluedMap<String, String> httpHeaders, Metadata metadata, ParseContext context) {
- for (ParseContextConfig config : configs) {
- config.configure(httpHeaders, metadata, context);
- }
- }
-}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/FetcherStreamFactory.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/FetcherStreamFactory.java
index d9b808b8aa..915fdc34ba 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/FetcherStreamFactory.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/FetcherStreamFactory.java
@@ -35,7 +35,6 @@ import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.RangeFetcher;
import org.apache.tika.pipes.core.fetcher.FetcherManager;
-import org.apache.tika.server.core.resource.TikaResource;
/**
* This class looks for "fetcherId" in the http header. If it is not null
@@ -88,7 +87,6 @@ public class FetcherStreamFactory implements InputStreamFactory {
fetchKey = getParam("fetchKeyLiteral", httpHeaders, params);
}
ParseContext parseContext = new ParseContext();
- TikaResource.fillParseContext(httpHeaders.getRequestHeaders(), metadata, parseContext);
long fetchRangeStart = getLong(getParam("fetchRangeStart", httpHeaders, params));
long fetchRangeEnd = getLong(getParam("fetchRangeEnd", httpHeaders, params));
if (StringUtils.isBlank(fetcherId) != StringUtils.isBlank(fetchKey)) {
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java
index 59ce60f35c..92b8b7cbd5 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/ServerStatusWatcher.java
@@ -67,7 +67,7 @@ public class ServerStatusWatcher implements Runnable {
LOG.error("Timeout task {}, millis elapsed {}; " +
"consider increasing the allowable time via: " +
"server config (\"taskTimeoutMillis\") or " +
- "per-request config (\"tika-task-timeout\" in parse-context)",
+ "per-request config (\"tika-task-timeout\")",
status.task.toString(), millisElapsed);
shutdown();
}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/DocumentSelectorConfig.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/DocumentSelectorConfig.java
deleted file mode 100644
index 27a2c868b0..0000000000
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/DocumentSelectorConfig.java
+++ /dev/null
@@ -1,46 +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.server.core.config;
-
-import jakarta.ws.rs.core.MultivaluedMap;
-import org.apache.commons.lang3.Strings;
-
-import org.apache.tika.extractor.DocumentSelector;
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.parser.ParseContext;
-import org.apache.tika.server.core.ParseContextConfig;
-
-public class DocumentSelectorConfig implements ParseContextConfig {
-
- public static final String X_TIKA_SKIP_EMBEDDED_HEADER = "X-Tika-Skip-Embedded";
-
- @Override
- public void configure(MultivaluedMap<String, String> httpHeaders, Metadata mtadata, ParseContext context) {
- DocumentSelector documentSelector = null;
- for (String key : httpHeaders.keySet()) {
- if (Strings.CS.endsWith(key, X_TIKA_SKIP_EMBEDDED_HEADER)) {
- String skipEmbedded = httpHeaders.getFirst(key);
- if (Boolean.parseBoolean(skipEmbedded)) {
- documentSelector = metadata -> false;
- }
- }
- }
- if (documentSelector != null) {
- context.set(DocumentSelector.class, documentSelector);
- }
- }
-}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/PasswordProviderConfig.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/PasswordProviderConfig.java
deleted file mode 100644
index 60438f19e4..0000000000
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/config/PasswordProviderConfig.java
+++ /dev/null
@@ -1,53 +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.server.core.config;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import jakarta.ws.rs.core.MultivaluedMap;
-import org.apache.commons.codec.binary.Base64;
-
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.parser.ParseContext;
-import org.apache.tika.parser.PasswordProvider;
-import org.apache.tika.server.core.ParseContextConfig;
-
-public class PasswordProviderConfig implements ParseContextConfig {
- public static final String PASSWORD = "Password";
- public static final String PASSWORD_BASE64_UTF8 = "Password_Base64_UTF-8";
- private static final Base64 BASE_64 = new Base64();
-
- private static String decodeBase64UTF8(String s) {
- byte[] bytes = BASE_64.decode(s);
- return new String(bytes, UTF_8);
- }
-
- @Override
- public void configure(MultivaluedMap<String, String> httpHeaders, Metadata metadata, ParseContext context) {
- String tmpPassword = httpHeaders.getFirst(PASSWORD_BASE64_UTF8);
- if (tmpPassword != null) {
- tmpPassword = decodeBase64UTF8(tmpPassword);
- } else {
- tmpPassword = httpHeaders.getFirst(PASSWORD);
- }
- if (tmpPassword != null) {
- final String password = tmpPassword;
- context.set(PasswordProvider.class, metadata1 -> password);
- }
- }
-
-}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java
index ce048cd6b9..6f884b165d 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/DetectorResource.java
@@ -29,7 +29,6 @@ import jakarta.ws.rs.core.UriInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.tika.config.TikaTaskTimeout;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
@@ -58,7 +57,6 @@ public class DetectorResource {
LOG.info("Detecting media type for Filename: {}", filename);
met.add(TikaCoreProperties.RESOURCE_NAME_KEY, filename);
ParseContext parseContext = new ParseContext();
- TikaResource.fillParseContext(httpHeaders.getRequestHeaders(), met, parseContext);
long timeoutMillis = TikaResource.getTaskTimeout(parseContext);
long taskId = serverStatus.start(ServerStatus.TASK.DETECT, filename, timeoutMillis);
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/MetadataResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/MetadataResource.java
index 73fec0051e..db73e480d4 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/MetadataResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/MetadataResource.java
@@ -17,10 +17,11 @@
package org.apache.tika.server.core.resource;
import static org.apache.tika.server.core.resource.TikaResource.fillMetadata;
-import static org.apache.tika.server.core.resource.TikaResource.fillParseContext;
+import static org.apache.tika.server.core.resource.TikaResource.setupMultipartConfig;
import java.io.IOException;
import java.io.InputStream;
+import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
@@ -59,6 +60,37 @@ public class MetadataResource {
.build();
}
+ /**
+ * Multipart endpoint with per-request ParseContext configuration.
+ * Accepts two parts: "file" (the document) and "config" (JSON configuration with parseContext).
+ */
+ @POST
+ @Consumes("multipart/form-data")
+ @Produces({"text/csv", "application/json"})
+ @Path("config")
+ public Response getMetadataWithConfig(
+ List<Attachment> attachments,
+ @Context HttpHeaders httpHeaders,
+ @Context UriInfo info) throws Exception {
+
+ Metadata metadata = new Metadata();
+ ParseContext context = new ParseContext();
+ try (InputStream tis = setupMultipartConfig(attachments, metadata, context)) {
+ // No need to parse embedded docs for metadata-only extraction
+ context.set(DocumentSelector.class, metadata1 -> false);
+
+ Parser parser = TikaResource.createParser();
+ TikaResource.logRequest(LOG, "/meta/config", metadata);
+ TikaResource.parse(parser, LOG, info.getPath(), tis, new LanguageHandler() {
+ public void endDocument() {
+ metadata.set("language", getLanguage().getLanguage());
+ }
+ }, metadata, context);
+
+ return Response.ok(metadata).build();
+ }
+ }
+
@PUT
@Produces({"text/csv", "application/json"})
public Response getMetadata(InputStream is, @Context HttpHeaders httpHeaders, @Context UriInfo info) throws Exception {
@@ -134,7 +166,6 @@ public class MetadataResource {
final ParseContext context = new ParseContext();
Parser parser = TikaResource.createParser();
fillMetadata(parser, metadata, httpHeaders);
- fillParseContext(httpHeaders, metadata, context);
//no need to parse embedded docs
context.set(DocumentSelector.class, metadata1 -> false);
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/RecursiveMetadataResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/RecursiveMetadataResource.java
index 5c75460cb0..fac10a3f73 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/RecursiveMetadataResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/RecursiveMetadataResource.java
@@ -17,8 +17,8 @@
package org.apache.tika.server.core.resource;
import static org.apache.tika.server.core.resource.TikaResource.fillMetadata;
-import static org.apache.tika.server.core.resource.TikaResource.fillParseContext;
import static org.apache.tika.server.core.resource.TikaResource.getTikaLoader;
+import static org.apache.tika.server.core.resource.TikaResource.setupMultipartConfig;
import java.io.InputStream;
import java.util.List;
@@ -39,6 +39,7 @@ import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.filter.MetadataFilter;
import org.apache.tika.parser.ParseContext;
@@ -66,7 +67,6 @@ public class RecursiveMetadataResource {
RecursiveParserWrapper wrapper = new RecursiveParserWrapper(parser);
fillMetadata(parser, metadata, httpHeaders);
- fillParseContext(httpHeaders, metadata, context);
TikaResource.logRequest(LOG, "/rmeta", metadata);
BasicContentHandlerFactory.HANDLER_TYPE type = handlerConfig.getType();
@@ -141,6 +141,58 @@ public class RecursiveMetadataResource {
.build();
}
+ /**
+ * Multipart endpoint with per-request ParseContext configuration.
+ * Accepts two parts: "file" (the document) and "config" (JSON configuration with parseContext).
+ */
+ @POST
+ @Consumes("multipart/form-data")
+ @Produces({"application/json"})
+ @Path("config{" + HANDLER_TYPE_PARAM + " : (/\\w+)?}")
+ public Response getMetadataWithConfig(
+ List<Attachment> attachments,
+ @Context HttpHeaders httpHeaders,
+ @Context UriInfo info,
+ @PathParam(HANDLER_TYPE_PARAM) String handlerTypeName) throws Exception {
+
+ Metadata metadata = new Metadata();
+ ParseContext context = new ParseContext();
+ try (TikaInputStream tis = setupMultipartConfig(attachments, metadata, context)) {
+
+ TikaResource.logRequest(LOG, "/rmeta/config", metadata);
+
+ return Response
+ .ok(parseMetadataWithContext(tis, metadata, httpHeaders.getRequestHeaders(), info,
+ buildHandlerConfig(httpHeaders.getRequestHeaders(), handlerTypeName != null ? handlerTypeName.substring(1) : null, HandlerConfig.PARSE_MODE.RMETA),
+ context))
+ .build();
+ }
+ }
+
+ private MetadataList parseMetadataWithContext(TikaInputStream tis, Metadata metadata, MultivaluedMap<String, String> httpHeaders,
+ UriInfo info, HandlerConfig handlerConfig, ParseContext context) throws Exception {
+ Parser parser = TikaResource.createParser();
+ RecursiveParserWrapper wrapper = new RecursiveParserWrapper(parser);
+
+ BasicContentHandlerFactory.HANDLER_TYPE type = handlerConfig.getType();
+ RecursiveParserWrapperHandler handler =
+ new RecursiveParserWrapperHandler(new BasicContentHandlerFactory(type, handlerConfig.getWriteLimit(), handlerConfig.isThrowOnWriteLimitReached(), context),
+ handlerConfig.getMaxEmbeddedResources(), TikaResource
+ .getTikaLoader()
+ .loadMetadataFilters());
+ try {
+ TikaResource.parse(wrapper, LOG, "/rmeta/config", tis, handler, metadata, context);
+ } catch (TikaServerParseException e) {
+ LOG.debug("server parse exception", e);
+ } catch (SecurityException | WebApplicationException e) {
+ throw e;
+ } catch (Exception e) {
+ LOG.error("something went seriously wrong", e);
+ }
+ MetadataFilter metadataFilter = context.get(MetadataFilter.class, getTikaLoader().loadMetadataFilters());
+ return new MetadataList(metadataFilter.filter(handler.getMetadataList()));
+ }
+
/**
* Returns an InputStream that can be deserialized as a list of
* {@link Metadata} objects.
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 b107c87d2d..53a8924c85 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
@@ -25,25 +25,18 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.StandardCopyOption;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -58,11 +51,8 @@ import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.core.UriInfo;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Strings;
import org.apache.cxf.attachment.ContentDisposition;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
-import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -76,20 +66,19 @@ import org.apache.tika.exception.EncryptedDocumentException;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.WriteLimitReachedException;
+import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
-import org.apache.tika.serialization.ParseContextDeserializer;
-import org.apache.tika.serialization.ParseContextUtils;
import org.apache.tika.sax.BasicContentHandlerFactory;
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.server.core.CompositeParseContextConfig;
+import org.apache.tika.serialization.ParseContextDeserializer;
+import org.apache.tika.serialization.ParseContextUtils;
import org.apache.tika.server.core.InputStreamFactory;
-import org.apache.tika.server.core.ParseContextConfig;
import org.apache.tika.server.core.ServerStatus;
import org.apache.tika.server.core.TikaServerConfig;
import org.apache.tika.server.core.TikaServerParseException;
@@ -108,8 +97,6 @@ public class TikaResource {
private static InputStreamFactory INPUTSTREAM_FACTORY = null;
private static ServerStatus SERVER_STATUS = null;
- private static ParseContextConfig PARSE_CONTEXT_CONFIG = new CompositeParseContextConfig();
-
public static void init(TikaLoader tikaLoader, TikaServerConfig tikaServerConfg, InputStreamFactory inputStreamFactory,
ServerStatus serverStatus) {
@@ -148,145 +135,34 @@ public class TikaResource {
return httpHeaders.getFirst("File-Name");
}
- public static void fillParseContext(MultivaluedMap<String, String> httpHeaders, Metadata metadata, ParseContext parseContext) {
- PARSE_CONTEXT_CONFIG.configure(httpHeaders, metadata, parseContext);
- }
-
- public static InputStream getInputStream(InputStream is, Metadata metadata, HttpHeaders headers, UriInfo uriInfo) {
- try {
- return INPUTSTREAM_FACTORY.getInputStream(is, metadata, headers, uriInfo);
- } catch (IOException e) {
- throw new TikaServerParseException(e);
- }
- }
-
/**
- * Utility method to set a property on a class via reflection.
+ * Parses config JSON and merges parseContext entries into the provided ParseContext.
*
- * @param object the <code>Object</code> to set the property on.
- * @param key the key of the HTTP Header.
- * @param val the value of HTTP header.
- * @param prefix the name of the HTTP Header prefix used to find property.
- * @throws WebApplicationException thrown when field cannot be found.
+ * @param configJson the JSON config string
+ * @param context the ParseContext to merge into
+ * @throws IOException if parsing fails
*/
- public static void processHeaderConfig(Object object, String key, String val, String prefix) {
- try {
- String property = Strings.CI.removeStart(key, prefix);
- Field field = null;
+ public static void mergeParseContextFromConfig(String configJson, ParseContext context) throws IOException {
+ 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);
+ ParseContextUtils.resolveAll(configuredContext, Thread.currentThread().getContextClassLoader());
+ for (Map.Entry<String, Object> entry : configuredContext.getContextMap().entrySet()) {
try {
- field = object
- .getClass()
- .getDeclaredField(StringUtils.uncapitalize(property));
- } catch (NoSuchFieldException e) {
- // try to match field case-insensitive way
- for (Field aField : object
- .getClass()
- .getDeclaredFields()) {
- if (aField
- .getName()
- .equalsIgnoreCase(property)) {
- field = aField;
- break;
- }
- }
- }
- String setter = field != null ? field.getName() : property;
- setter = "set" + setter
- .substring(0, 1)
- .toUpperCase(Locale.US) + setter.substring(1);
- //default assume string class
- //if there's a more specific type, e.g. double, int, boolean
- //try that.
- Class clazz = String.class;
- if (field != null) {
- if (field.getType() == int.class || field.getType() == Integer.class) {
- clazz = int.class;
- } else if (field.getType() == double.class) {
- clazz = double.class;
- } else if (field.getType() == Double.class) {
- clazz = Double.class;
- } else if (field.getType() == float.class) {
- clazz = float.class;
- } else if (field.getType() == Float.class) {
- clazz = Float.class;
- } else if (field.getType() == boolean.class) {
- clazz = boolean.class;
- } else if (field.getType() == Boolean.class) {
- clazz = Boolean.class;
- } else if (field.getType() == long.class) {
- clazz = long.class;
- } else if (field.getType() == Long.class) {
- clazz = Long.class;
- }
+ Class<?> clazz = Class.forName(entry.getKey());
+ context.set((Class) clazz, entry.getValue());
+ } catch (ClassNotFoundException e) {
+ LOG.warn("Could not load class for parseContext entry: {}", entry.getKey());
}
-
- Method m = tryToGetMethod(object, setter, clazz);
- //if you couldn't find more specific setter, back off
- //to string setter and try that.
- if (m == null && clazz != String.class) {
- m = tryToGetMethod(object, setter, String.class);
- }
-
- if (m != null) {
- if (clazz == String.class) {
- checkTrustWorthy(setter, val);
- m.invoke(object, val);
- } else if (clazz == int.class || clazz == Integer.class) {
- m.invoke(object, Integer.parseInt(val));
- } else if (clazz == double.class || clazz == Double.class) {
- m.invoke(object, Double.parseDouble(val));
- } else if (clazz == boolean.class || clazz == Boolean.class) {
- m.invoke(object, Boolean.parseBoolean(val));
- } else if (clazz == float.class || clazz == Float.class) {
- m.invoke(object, Float.parseFloat(val));
- } else if (clazz == long.class || clazz == Long.class) {
- m.invoke(object, Long.parseLong(val));
- } else {
- throw new IllegalArgumentException("setter must be String, int, float, double or boolean...for now");
- }
- } else {
- throw new NoSuchMethodException("Couldn't find: " + setter);
- }
-
- } catch (Throwable ex) {
- // TIKA-3345
- String error = (!(ex.getCause() instanceof IllegalArgumentException)) ? String.format(Locale.ROOT, "%s is an invalid %s header", key, prefix) :
- String.format(Locale.ROOT, "%s is an invalid %s header value", val, key);
- throw new WebApplicationException(error, Response.Status.BAD_REQUEST);
}
}
- private static void checkTrustWorthy(String setter, String val) {
- if (setter == null || val == null) {
- throw new IllegalArgumentException("setter and val must not be null");
- }
- if (setter
- .toLowerCase(Locale.US)
- .contains("trusted")) {
- throw new IllegalArgumentException("Can't call a trusted method via tika-server headers");
- }
- Matcher m = ALLOWABLE_HEADER_CHARS.matcher(val);
- if (!m.find()) {
- throw new IllegalArgumentException("Header val: " + val + " contains illegal characters. " + "Must contain: TikaResource.ALLOWABLE_HEADER_CHARS");
- }
- }
-
- /**
- * Tries to get method. Silently swallows NoMethodException and returns
- * <code>null</code> if not found.
- *
- * @param object the object to get method from.
- * @param method the name of the method to get.
- * @param clazz the parameter type of the method to get.
- * @return the found method instance
- */
- private static Method tryToGetMethod(Object object, String method, Class clazz) {
+ public static InputStream getInputStream(InputStream is, Metadata metadata, HttpHeaders headers, UriInfo uriInfo) {
try {
- return object
- .getClass()
- .getMethod(method, clazz);
- } catch (NoSuchMethodException e) {
- return null;
+ return INPUTSTREAM_FACTORY.getInputStream(is, metadata, headers, uriInfo);
+ } catch (IOException e) {
+ throw new TikaServerParseException(e);
}
}
@@ -330,6 +206,69 @@ public class TikaResource {
}
}
+ /**
+ * Processes multipart attachments for /config endpoints.
+ * Extracts the "file" and optional "config" attachments, sets up metadata
+ * (filename, content-type) from the file attachment, and processes any
+ * config JSON into the ParseContext.
+ *
+ * @param attachments the multipart attachments
+ * @param metadata metadata to populate with filename and content-type
+ * @param context parse context to populate from config JSON
+ * @return TikaInputStream wrapping the file attachment's content
+ * @throws IOException if file attachment is missing or config processing fails
+ */
+ public static TikaInputStream setupMultipartConfig(List<Attachment> attachments,
+ Metadata metadata,
+ ParseContext context) throws IOException {
+ Attachment fileAtt = null;
+ Attachment configAtt = null;
+
+ for (Attachment att : attachments) {
+ ContentDisposition cd = att.getContentDisposition();
+ if (cd != null) {
+ String name = cd.getParameter("name");
+ if ("file".equals(name)) {
+ fileAtt = att;
+ } else if ("config".equals(name)) {
+ configAtt = att;
+ }
+ }
+ }
+
+ if (fileAtt == null) {
+ throw new IOException("Missing 'file' attachment");
+ }
+
+ // Set filename from content-disposition
+ ContentDisposition cd = fileAtt.getContentDisposition();
+ if (cd != null) {
+ String filename = cd.getParameter("filename");
+ if (filename != null) {
+ metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, filename);
+ }
+ }
+
+ // Set content-type from the file attachment (not the multipart request headers)
+ if (fileAtt.getContentType() != null) {
+ String contentType = fileAtt.getContentType().toString();
+ if (contentType != null && !contentType.startsWith("multipart/") &&
+ !"application/octet-stream".equals(contentType)) {
+ metadata.add(Metadata.CONTENT_TYPE, contentType);
+ metadata.add(TikaCoreProperties.CONTENT_TYPE_USER_OVERRIDE, contentType);
+ }
+ }
+
+ // Process config JSON if provided
+ if (configAtt != null) {
+ String configJson = new String(configAtt.getObject(InputStream.class).readAllBytes(),
+ StandardCharsets.UTF_8);
+ mergeParseContextFromConfig(configJson, context);
+ }
+
+ return TikaInputStream.get(fileAtt.getObject(InputStream.class));
+ }
+
/**
* Use this to call a parser and unify exception handling.
* NOTE: This call to parse closes the InputStream. DO NOT surround
@@ -419,51 +358,42 @@ public class TikaResource {
@Produces("text/plain")
@Path("form")
public StreamingOutput getTextFromMultipart(Attachment att, @Context HttpHeaders httpHeaders, @Context final UriInfo info) throws TikaConfigException, IOException {
- LOG.info("===== getTextFromMultipart (single Attachment) CALLED =====");
return produceText(att.getObject(InputStream.class), new Metadata(), preparePostHeaderMap(att, httpHeaders), info);
}
- // Greenfield test endpoint for multipart with config
+ /**
+ * Multipart endpoint with per-request JSON configuration.
+ * Accepts two parts: "file" (the document) and "config" (JSON configuration).
+ * <p>
+ * The config JSON should contain parser configs at the root level, e.g.:
+ * <pre>
+ * {
+ * "pdf-parser": { "ocrStrategy": "no_ocr" },
+ * "tesseract-ocr-parser": { "language": "eng" }
+ * }
+ * </pre>
+ */
@POST
@Consumes("multipart/form-data")
@Produces("text/plain")
- @Path("test-config")
- public StreamingOutput testMultipartWithConfig(
+ @Path("config")
+ public StreamingOutput getTextWithConfig(
List<Attachment> attachments,
@Context HttpHeaders httpHeaders,
@Context final UriInfo info) throws TikaConfigException, IOException {
- LOG.info("===== testMultipartWithConfig CALLED with {} attachments =====", attachments.size());
-
- // Find the file and config attachments
- Attachment fileAtt = null;
- Attachment configAtt = null;
- for (Attachment att : attachments) {
- ContentDisposition cd = att.getContentDisposition();
- if (cd != null) {
- String name = cd.getParameter("name");
- LOG.info("Found attachment with name: {}", name);
- if ("file".equals(name)) {
- fileAtt = att;
- } else if ("config".equals(name)) {
- configAtt = att;
- }
- }
- }
+ final Metadata metadata = new Metadata();
+ final ParseContext context = new ParseContext();
+ final TikaInputStream tis = setupMultipartConfig(attachments, metadata, context);
- if (fileAtt == null) {
- throw new IllegalArgumentException("Missing 'file' attachment");
- }
- if (configAtt == null) {
- throw new IllegalArgumentException("Missing 'config' attachment");
- }
+ final Parser parser = createParser();
+ logRequest(LOG, "/tika/config", metadata);
- final Metadata metadata = new Metadata();
- MultivaluedMap<String, String> headers = preparePostHeaderMap(fileAtt, httpHeaders);
- return produceTextWithConfig(
- getInputStream(fileAtt.getObject(InputStream.class), metadata, httpHeaders, info),
- configAtt.getObject(InputStream.class),
- metadata, headers, info);
+ return outputStream -> {
+ Writer writer = new OutputStreamWriter(outputStream, UTF_8);
+ BodyContentHandler body = new BodyContentHandler(new RichTextContentHandler(writer));
+ parse(parser, LOG, info.getPath(), tis, body, metadata, context);
+ };
}
//this is equivalent to text-main in tika-app
@@ -490,7 +420,6 @@ public class TikaResource {
final ParseContext context = new ParseContext();
fillMetadata(parser, metadata, httpHeaders);
- fillParseContext(httpHeaders, metadata, context);
logRequest(LOG, "/tika", metadata);
@@ -507,70 +436,16 @@ public class TikaResource {
@Consumes("*/*")
@Produces("text/plain")
public StreamingOutput getText(final InputStream is, @Context HttpHeaders httpHeaders, @Context final UriInfo info) throws TikaConfigException, IOException {
- LOG.info("===== getText (PUT, no @Path) CALLED =====");
final Metadata metadata = new Metadata();
return produceText(getInputStream(is, metadata, httpHeaders, info), metadata, httpHeaders.getRequestHeaders(), info);
}
- /**
- * Produces text output with per-request ParseContext configuration.
- * Extracts only the parse-context section from the config to allow per-request
- * configuration of ParseContext objects (e.g., timeout, handler settings).
- * Uses the server's configured parser to preserve any parser configuration from startup.
- */
- private StreamingOutput produceTextWithConfig(final InputStream fileStream, InputStream configStream,
- final Metadata metadata, MultivaluedMap<String, String> httpHeaders, final UriInfo info)
- throws TikaConfigException, IOException {
-
- // Use the server's configured parser (not a new one from the config)
- final Parser parser = createParser();
- final ParseContext context = new ParseContext();
-
- // Read the config JSON to extract only the parse-context section
- String configJson = new String(configStream.readAllBytes(), StandardCharsets.UTF_8);
- ObjectMapper mapper = new ObjectMapper();
- JsonNode rootNode = mapper.readTree(configJson);
-
- JsonNode parseContextNode = rootNode.get("parse-context");
- LOG.info("found parseContext: " + parseContextNode);
- if (parseContextNode != null) {
- // Deserialize parseContext section
- ParseContext configuredContext = ParseContextDeserializer.readParseContext(parseContextNode);
-
- // Resolve all friendly-named components from ConfigContainer
- // For example: "tika-task-timeout" -> TikaTaskTimeout instance
- ParseContextUtils.resolveAll(configuredContext, Thread.currentThread().getContextClassLoader());
-
- // Merge configured context into our context
- for (Map.Entry<String, Object> entry : configuredContext.getContextMap().entrySet()) {
- try {
- Class<?> clazz = Class.forName(entry.getKey());
- context.set((Class) clazz, entry.getValue());
- } catch (ClassNotFoundException e) {
- LOG.warn("Could not load class for parseContext entry: " + entry.getKey(), e);
- }
- }
- }
-
- fillMetadata(parser, metadata, httpHeaders);
- fillParseContext(httpHeaders, metadata, context);
-
- logRequest(LOG, "/tika (with config)", metadata);
-
- return outputStream -> {
- Writer writer = new OutputStreamWriter(outputStream, UTF_8);
- BodyContentHandler body = new BodyContentHandler(new RichTextContentHandler(writer));
- parse(parser, LOG, info.getPath(), fileStream, body, metadata, context);
- };
- }
-
public StreamingOutput produceText(final InputStream is, final Metadata metadata, MultivaluedMap<String, String> httpHeaders, final UriInfo info)
throws TikaConfigException, IOException {
final Parser parser = createParser();
final ParseContext context = new ParseContext();
fillMetadata(parser, metadata, httpHeaders);
- fillParseContext(httpHeaders, metadata, context);
logRequest(LOG, "/tika", metadata);
@@ -660,7 +535,6 @@ public class TikaResource {
final ParseContext context = new ParseContext();
fillMetadata(parser, metadata, httpHeaders);
- fillParseContext(httpHeaders, metadata, context);
logRequest(LOG, "/tika", metadata);
int writeLimit = -1;
@@ -708,8 +582,6 @@ public class TikaResource {
final ParseContext context = new ParseContext();
fillMetadata(parser, metadata, httpHeaders);
- fillParseContext(httpHeaders, metadata, context);
-
logRequest(LOG, "/tika", metadata);
@@ -744,13 +616,14 @@ public class TikaResource {
/**
* Prepares a multivalued map, combining attachment headers and request headers.
- * Gives priority to attachment headers.
+ * For multipart requests, the attachment's Content-Type takes priority over the
+ * request's Content-Type (which is multipart/form-data).
*
* @param att the attachment.
* @param httpHeaders the http headers, fetched from context.
* @return the case insensitive MetadataMap containing combined headers.
*/
- private MetadataMap<String, String> preparePostHeaderMap(Attachment att, HttpHeaders httpHeaders) {
+ public static MetadataMap<String, String> preparePostHeaderMap(Attachment att, HttpHeaders httpHeaders) {
if (att == null && httpHeaders == null) {
return null;
}
@@ -761,6 +634,22 @@ public class TikaResource {
if (att != null && att.getHeaders() != null) {
finalHeaders.putAll(att.getHeaders());
}
+ // For multipart, get the attachment's Content-Type which overrides the request's
+ // multipart/form-data Content-Type. Check multiple sources:
+ if (att != null) {
+ String attachmentContentType = null;
+ // First try getContentType() which returns the MediaType set via constructor
+ if (att.getContentType() != null) {
+ attachmentContentType = att.getContentType().toString();
+ }
+ // Also check the attachment's headers directly
+ if (attachmentContentType == null && att.getHeaders() != null) {
+ attachmentContentType = att.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
+ }
+ if (attachmentContentType != null && !attachmentContentType.startsWith("multipart/")) {
+ finalHeaders.putSingle(HttpHeaders.CONTENT_TYPE, attachmentContentType);
+ }
+ }
return finalHeaders;
}
}
diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/UnpackerResource.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/UnpackerResource.java
index 5db722edd3..74918ee9b6 100644
--- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/UnpackerResource.java
+++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/UnpackerResource.java
@@ -18,7 +18,7 @@ package org.apache.tika.server.core.resource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.tika.server.core.resource.TikaResource.fillMetadata;
-import static org.apache.tika.server.core.resource.TikaResource.fillParseContext;
+import static org.apache.tika.server.core.resource.TikaResource.setupMultipartConfig;
import java.io.IOException;
import java.io.InputStream;
@@ -27,9 +27,12 @@ import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@@ -45,6 +48,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.mutable.MutableInt;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
@@ -104,6 +108,87 @@ public class UnpackerResource {
return process(TikaResource.getInputStream(is, new Metadata(), httpHeaders, info), httpHeaders, info, true);
}
+ /**
+ * Multipart endpoint with per-request ParseContext configuration.
+ * Accepts two parts: "file" (the document) and "config" (JSON configuration with parseContext).
+ */
+ @Path("/config")
+ @POST
+ @Consumes("multipart/form-data")
+ @Produces({"application/zip", "application/x-tar"})
+ public Map<String, byte[]> unpackWithConfig(List<Attachment> attachments, @Context HttpHeaders httpHeaders, @Context UriInfo info) throws Exception {
+ return processWithConfig(attachments, httpHeaders, info, false);
+ }
+
+ /**
+ * Multipart endpoint with per-request ParseContext configuration that extracts all content.
+ * Accepts two parts: "file" (the document) and "config" (JSON configuration with parseContext).
+ */
+ @Path("/all/config")
+ @POST
+ @Consumes("multipart/form-data")
+ @Produces({"application/zip", "application/x-tar"})
+ public Map<String, byte[]> unpackAllWithConfig(List<Attachment> attachments, @Context HttpHeaders httpHeaders, @Context UriInfo info) throws Exception {
+ return processWithConfig(attachments, httpHeaders, info, true);
+ }
+
+ private Map<String, byte[]> processWithConfig(List<Attachment> attachments, HttpHeaders httpHeaders, UriInfo info, boolean saveAll) throws Exception {
+ Metadata metadata = new Metadata();
+ ParseContext pc = new ParseContext();
+ try (InputStream tis = setupMultipartConfig(attachments, metadata, pc)) {
+ return processWithContext(tis, metadata, pc, info, saveAll);
+ }
+ }
+
+ private Map<String, byte[]> processWithContext(InputStream is, Metadata metadata, ParseContext pc, UriInfo info, boolean saveAll) throws Exception {
+ long unpackMaxBytes = DEFAULT_MAX_ATTACHMENT_BYTES;
+
+ Parser parser = TikaResource.createParser();
+ if (parser instanceof DigestingParser) {
+ //no need to digest for unwrapping
+ parser = ((DigestingParser) parser).getWrappedParser();
+ }
+
+ TikaResource.logRequest(LOG, "/unpack/config", metadata);
+ //even though we aren't currently parsing embedded documents,
+ //we need to add this to allow for "inline" use of other parsers.
+ pc.set(Parser.class, parser);
+ ContentHandler ch;
+ UnsynchronizedByteArrayOutputStream text = UnsynchronizedByteArrayOutputStream
+ .builder()
+ .get();
+
+ if (saveAll) {
+ ch = new BodyContentHandler(new RichTextContentHandler(new OutputStreamWriter(text, UTF_8)));
+ } else {
+ ch = new DefaultHandler();
+ }
+
+ Map<String, byte[]> files = new HashMap<>();
+ MutableInt count = new MutableInt();
+
+ pc.set(EmbeddedDocumentExtractor.class, new MyEmbeddedDocumentExtractor(count, files, unpackMaxBytes));
+
+ TikaResource.parse(parser, LOG, info.getPath(), is, ch, metadata, pc);
+
+ if (count.intValue() == 0 && !saveAll) {
+ throw new WebApplicationException(Response.Status.NO_CONTENT);
+ }
+
+ if (saveAll) {
+ files.put(TEXT_FILENAME, text.toByteArray());
+
+ UnsynchronizedByteArrayOutputStream metaStream = UnsynchronizedByteArrayOutputStream
+ .builder()
+ .get();
+ metadataToCsv(metadata, metaStream);
+
+ files.put(META_FILENAME, metaStream.toByteArray());
+ }
+
+ return files;
+ }
+
private Map<String, byte[]> process(InputStream is, @Context HttpHeaders httpHeaders, @Context UriInfo info, boolean saveAll) throws Exception {
Metadata metadata = new Metadata();
ParseContext pc = new ParseContext();
@@ -125,7 +210,6 @@ public class UnpackerResource {
parser = ((DigestingParser) parser).getWrappedParser();
}
fillMetadata(parser, metadata, httpHeaders.getRequestHeaders());
- fillParseContext(httpHeaders.getRequestHeaders(), metadata, pc);
TikaResource.logRequest(LOG, "/unpack", metadata);
//even though we aren't currently parsing embedded documents,
diff --git a/tika-server/tika-server-core/src/main/resources/META-INF/services/org.apache.tika.server.core.ParseContextConfig b/tika-server/tika-server-core/src/main/resources/META-INF/services/org.apache.tika.server.core.ParseContextConfig
deleted file mode 100644
index b1bd8ce2ed..0000000000
--- a/tika-server/tika-server-core/src/main/resources/META-INF/services/org.apache.tika.server.core.ParseContextConfig
+++ /dev/null
@@ -1,17 +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.
-org.apache.tika.server.core.config.PasswordProviderConfig
-org.apache.tika.server.core.config.DocumentSelectorConfig
-org.apache.tika.server.core.config.TimeoutConfig
\ No newline at end of file
diff --git a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
index 50b341eaab..be2287c278 100644
--- a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
+++ b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerIntegrationTest.java
@@ -42,16 +42,11 @@ import org.apache.cxf.configuration.security.KeyManagersType;
import org.apache.cxf.configuration.security.KeyStoreType;
import org.apache.cxf.configuration.security.TrustManagersType;
import org.apache.cxf.jaxrs.client.WebClient;
-import org.apache.cxf.jaxrs.ext.multipart.Attachment;
-import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
-import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.transport.http.HTTPConduit;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.serialization.JsonMetadataList;
@@ -59,7 +54,6 @@ import org.apache.tika.utils.ProcessUtils;
public class TikaServerIntegrationTest extends IntegrationTestBase {
- private static final Logger LOG = LoggerFactory.getLogger(TikaServerIntegrationTest.class);
private static Path TLS_KEYS;
@TempDir
@@ -122,65 +116,6 @@ public class TikaServerIntegrationTest extends IntegrationTestBase {
testStopped(2000);
}
- /**
- * Test legacy header-based timeout configuration.
- * @deprecated This tests the old header-based approach. Use testTaskTimeoutMultipart instead.
- */
- @Test
- public void testTaskTimeoutHeader() throws Exception {
-
- startProcess(new String[]{"-config", getConfig("tika-config-server-basic.json")});
- awaitServerStartup();
- Response response = null;
- try {
- response = WebClient
- .create(endPoint + RMETA_PATH)
- .accept("application/json")
- .header(TimeoutConfig.X_TIKA_TIMEOUT_MILLIS, 100)
- .put(ClassLoader.getSystemResourceAsStream(TEST_HEAVY_HANG));
- } catch (Exception e) {
- //oom may or may not cause an exception depending
- //on the timing
- }
- //give some time for the server to crash/terminate itself
- testStopped(2000);
- }
-
- /**
- * Test JSON-based timeout configuration via multipart request.
- * This is the new recommended approach for configuring timeouts.
- */
- @Test
- public void testTaskTimeoutMultipart() throws Exception {
- startProcess(new String[]{"-config", getConfig("tika-config-server-basic.json")});
- awaitServerStartup();
-
- // Create multipart form with file and config
- ContentDisposition fileCD = new ContentDisposition("form-data; name=\"file\"; filename=\"heavy_hang_30000.xml\"");
- ContentDisposition configCD = new ContentDisposition("form-data; name=\"config\"; filename=\"tika-config-timeout-100ms.json\"");
-
- MultipartBody multipart = new MultipartBody(List.of(
- new Attachment("file", ClassLoader.getSystemResourceAsStream(TEST_HEAVY_HANG), fileCD),
- new Attachment("config", ClassLoader.getSystemResourceAsStream("configs/tika-config-timeout-100ms.json"), configCD)
- ));
-
- Response response = null;
- try {
- LOG.info("TEST: Sending request to: {}", endPoint + "/tika/test-config");
- response = WebClient
- .create(endPoint + "/tika/test-config")
- .accept("text/plain")
- .type("multipart/form-data")
- .post(multipart);
- LOG.info("TEST: Response status: {}", response != null ? response.getStatus() : "null");
- } catch (Exception e) {
- LOG.info("TEST: Exception during request", e);
- //timeout may or may not cause an exception depending on timing
- }
- //give some time for the server to crash/terminate itself
- testStopped(2000);
- }
-
private void testStopped(long millis) throws InterruptedException {
Thread.sleep(millis);
try {
diff --git a/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/config/PDFServerConfig.java b/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/config/PDFServerConfig.java
deleted file mode 100644
index e38ca69099..0000000000
--- a/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/config/PDFServerConfig.java
+++ /dev/null
@@ -1,70 +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.server.standard.config;
-
-import static org.apache.tika.server.core.resource.TikaResource.processHeaderConfig;
-
-import java.util.List;
-import java.util.Map;
-
-import jakarta.ws.rs.core.MultivaluedMap;
-import org.apache.commons.lang3.Strings;
-
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.parser.ParseContext;
-import org.apache.tika.parser.pdf.PDFParserConfig;
-import org.apache.tika.server.core.ParseContextConfig;
-
-/**
- * PDF parser configuration, for the request
- */
-public class PDFServerConfig implements ParseContextConfig {
-
- /**
- * The HTTP header prefix required (case-insensitive) by this config.
- */
- public static final String X_TIKA_PDF_HEADER_PREFIX = "X-Tika-PDF";
-
- /**
- * Configures the parseContext with present headers.
- * Note: only first value of header is considered.
- *
- * @param httpHeaders the headers.
- * @param metadata the metadata.
- * @param parseContext the parse context to configure.
- */
- @Override
- public void configure(MultivaluedMap<String, String> httpHeaders, Metadata metadata, ParseContext parseContext) {
- //lazily initialize configs
- //if a header is submitted, any params set in --tika-config tika-config.xml
- //upon server startup will be ignored.
- PDFParserConfig pdfParserConfig = null;
- for (Map.Entry<String, List<String>> kvp : httpHeaders.entrySet()) {
- if (Strings.CI.startsWith(kvp.getKey(), X_TIKA_PDF_HEADER_PREFIX)) {
- pdfParserConfig = (pdfParserConfig == null) ? new PDFParserConfig() : pdfParserConfig;
- processHeaderConfig(pdfParserConfig, kvp.getKey(), kvp
- .getValue()
- .get(0)
- .trim(), X_TIKA_PDF_HEADER_PREFIX);
- }
- }
- if (pdfParserConfig != null) {
- parseContext.set(PDFParserConfig.class, pdfParserConfig);
- }
- }
-
-}
diff --git a/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/config/TesseractServerConfig.java b/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/config/TesseractServerConfig.java
deleted file mode 100644
index 016d3c50d1..0000000000
--- a/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/config/TesseractServerConfig.java
+++ /dev/null
@@ -1,74 +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.server.standard.config;
-
-import static org.apache.tika.server.core.resource.TikaResource.processHeaderConfig;
-
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-import jakarta.ws.rs.core.MultivaluedMap;
-import org.apache.commons.lang3.Strings;
-
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.parser.ParseContext;
-import org.apache.tika.parser.ocr.TesseractOCRConfig;
-import org.apache.tika.server.core.ParseContextConfig;
-
-/**
- * Tesseract configuration, for the request
- */
-public class TesseractServerConfig implements ParseContextConfig {
-
- /**
- * The HTTP header prefix required (case-insensitive) by this config.
- */
- public static final String X_TIKA_OCR_HEADER_PREFIX = "X-Tika-OCR";
-
- /**
- * Configures the parseContext with present headers.
- * Note: only first value of header is considered.
- *
- * @param httpHeaders the headers.
- * @param metadata the metadata.
- * @param parseContext the parse context to configure.
- */
- @Override
- public void configure(MultivaluedMap<String, String> httpHeaders, Metadata metadata, ParseContext parseContext) {
- //lazily initialize configs
- //if a header is submitted, any params set in --tika-config tika-config.xml
- //upon server startup will be ignored.
- TesseractOCRConfig ocrConfig = null;
- for (Map.Entry<String, List<String>> kvp : httpHeaders.entrySet()) {
- if (Strings.CI.startsWith(kvp.getKey(), X_TIKA_OCR_HEADER_PREFIX)) {
- ocrConfig = (ocrConfig == null) ? new TesseractOCRConfig() : ocrConfig;
- if (kvp.getKey().toLowerCase(Locale.ROOT).contains("path")) {
- throw new IllegalArgumentException("Can't change the paths at runtime.");
- }
- processHeaderConfig(ocrConfig, kvp.getKey(), kvp
- .getValue()
- .get(0)
- .trim(), X_TIKA_OCR_HEADER_PREFIX);
- }
- }
- if (ocrConfig != null) {
- parseContext.set(TesseractOCRConfig.class, ocrConfig);
- }
- }
-
-}
diff --git a/tika-server/tika-server-standard/src/main/resources/META-INF/services/org.apache.tika.server.core.ParseContextConfig b/tika-server/tika-server-standard/src/main/resources/META-INF/services/org.apache.tika.server.core.ParseContextConfig
deleted file mode 100644
index 0a27a4ae33..0000000000
--- a/tika-server/tika-server-standard/src/main/resources/META-INF/services/org.apache.tika.server.core.ParseContextConfig
+++ /dev/null
@@ -1,16 +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.
-org.apache.tika.server.standard.config.PDFServerConfig
-org.apache.tika.server.standard.config.TesseractServerConfig
\ No newline at end of file
diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/MetadataResourceTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/MetadataResourceTest.java
index 2184635463..1b2dba3d6e 100644
--- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/MetadataResourceTest.java
+++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/MetadataResourceTest.java
@@ -24,6 +24,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -36,6 +37,9 @@ import org.apache.commons.csv.CSVRecord;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.junit.jupiter.api.Test;
@@ -98,47 +102,72 @@ public class MetadataResourceTest extends CXFTestBase {
@Test
public void testPasswordProtected() throws Exception {
+ // Test 1: No password - should fail
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.xls\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED), fileCd);
+
Response response = WebClient
- .create(endPoint + META_PATH)
- .type("application/vnd.ms-excel")
- .accept("text/csv")
- .put(ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED));
+ .create(endPoint + META_PATH + "/config")
+ .type("multipart/form-data")
+ .accept("application/json")
+ .post(new MultipartBody(Arrays.asList(fileAtt)));
- // Won't work, no password given
+ System.out.println(IOUtils.toString((InputStream) response.getEntity(), UTF_8.name()));
+ // Won't work, no password given - EncryptedDocumentException returns 422
assertEquals(500, response.getStatus());
- // Try again, this time with the wrong password
+ // Test 2: Wrong password - should fail
+ fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.xls\"");
+ fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED), fileCd);
+ String wrongConfigJson = """
+ {
+ "simple-password-provider": {
+ "password": "wrong password"
+ }
+ }
+ """;
+ ContentDisposition configCd = new ContentDisposition("form-data; name=\"config\"; filename=\"config.json\"");
+ Attachment wrongConfigAtt = new Attachment("config",
+ new java.io.ByteArrayInputStream(wrongConfigJson.getBytes(UTF_8)), configCd);
+
response = WebClient
- .create(endPoint + META_PATH)
- .type("application/vnd.ms-excel")
- .accept("text/csv")
- .header("Password", "wrong password")
- .put(ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED));
+ .create(endPoint + META_PATH + "/config")
+ .type("multipart/form-data")
+ .accept("application/json")
+ .post(new MultipartBody(Arrays.asList(fileAtt, wrongConfigAtt)));
assertEquals(500, response.getStatus());
- // Try again, this time with the password
+ // Test 3: Correct password - should work
+ fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.xls\"");
+ fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED), fileCd);
+ String configJson = """
+ {
+ "simple-password-provider": {
+ "password": "password"
+ }
+ }
+ """;
+ configCd = new ContentDisposition("form-data; name=\"config\"; filename=\"config.json\"");
+ Attachment configAtt = new Attachment("config",
+ new java.io.ByteArrayInputStream(configJson.getBytes(UTF_8)), configCd);
+
response = WebClient
- .create(endPoint + META_PATH)
- .type("application/vnd.ms-excel")
- .accept("text/csv")
- .header("Password", "password")
- .put(ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED));
+ .create(endPoint + META_PATH + "/config")
+ .type("multipart/form-data")
+ .accept("application/json")
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
// Will work
assertEquals(200, response.getStatus());
// Check results
- Map<String, String> metadata = new HashMap<>();
- try (Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
- CSVParser csvReader = CSVParser.builder().setReader(reader).setFormat(CSVFormat.EXCEL).get()) {
- for (CSVRecord r : csvReader) {
- metadata.put(r.get(0), r.get(1));
- }
- }
-
- assertNotNull(metadata.get(TikaCoreProperties.CREATOR.getName()));
- assertEquals("pavel", metadata.get(TikaCoreProperties.CREATOR.getName()));
+ Metadata metadata = JsonMetadata.fromJson(new InputStreamReader((InputStream) response.getEntity(), UTF_8));
+ assertNotNull(metadata.get(TikaCoreProperties.CREATOR));
+ assertEquals("pavel", metadata.get(TikaCoreProperties.CREATOR));
}
@Test
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 fa49ff67eb..33aae76b85 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
@@ -26,6 +26,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import jakarta.ws.rs.core.MultivaluedHashMap;
@@ -35,6 +36,7 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.junit.jupiter.api.Test;
@@ -45,7 +47,6 @@ import org.apache.tika.metadata.PDF;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.serialization.JsonMetadataList;
import org.apache.tika.server.core.CXFTestBase;
-import org.apache.tika.server.core.config.DocumentSelectorConfig;
import org.apache.tika.server.core.resource.RecursiveMetadataResource;
import org.apache.tika.server.core.writer.MetadataListMessageBodyWriter;
@@ -179,15 +180,14 @@ public class RecursiveMetadataResourceTest extends CXFTestBase {
@Test
public void testPasswordProtected() throws Exception {
+ // Test that encrypted document without password shows error
Response response = WebClient
.create(endPoint + META_PATH)
.type("application/vnd.ms-excel")
.accept("application/json")
.put(ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED));
- // Won't work, no password given
assertEquals(200, response.getStatus());
- // Check results
Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
@@ -197,18 +197,29 @@ public class RecursiveMetadataResourceTest extends CXFTestBase {
assertContains("org.apache.tika.exception.EncryptedDocumentException", metadataList
.get(0)
.get(TikaCoreProperties.CONTAINER_EXCEPTION));
- // Try again, this time with the password
+
+ // Test with password via JSON config
+ String configJson = """
+ {
+ "simple-password-provider": {
+ "password": "password"
+ }
+ }
+ """;
+ Attachment fileAtt = new Attachment("file", "application/vnd.ms-excel",
+ ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED));
+
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(UTF_8)));
+
response = WebClient
- .create(endPoint + META_PATH)
- .type("application/vnd.ms-excel")
+ .create(endPoint + META_PATH + "/config")
+ .type("multipart/form-data")
.accept("application/json")
- .header("Password", "password")
- .put(ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
- // Will work
assertEquals(200, response.getStatus());
- // Check results
reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
metadataList = JsonMetadataList.fromJson(reader);
assertNotNull(metadataList
@@ -415,27 +426,8 @@ public class RecursiveMetadataResourceTest extends CXFTestBase {
}
}
- // TIKA-3227
- @Test
- public void testSkipEmbedded() throws Exception {
- Response response = WebClient
- .create(endPoint + META_PATH)
- .accept("application/json")
- .header(DocumentSelectorConfig.X_TIKA_SKIP_EMBEDDED_HEADER, "false")
- .put(ClassLoader.getSystemResourceAsStream(TEST_RECURSIVE_DOC));
- Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
- List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
- assertEquals(12, metadataList.size());
-
- response = WebClient
- .create(endPoint + META_PATH)
- .accept("application/json")
- .header(DocumentSelectorConfig.X_TIKA_SKIP_EMBEDDED_HEADER, "true")
- .put(ClassLoader.getSystemResourceAsStream(TEST_RECURSIVE_DOC));
- reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
- metadataList = JsonMetadataList.fromJson(reader);
- assertEquals(1, metadataList.size());
- }
+ // TIKA-3227 - TODO: re-enable once HandlerConfig is configurable via JSON
+ // Use maxEmbeddedResources=0 in handler-config to skip embedded documents
@Test
public void testWriteLimit() throws Exception {
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 41db6ba73e..ac82d820ab 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
@@ -55,7 +55,6 @@ import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
-import org.apache.tika.parser.pdf.PDFParserConfig;
import org.apache.tika.pipes.api.FetchEmitTuple;
import org.apache.tika.pipes.api.HandlerConfig;
import org.apache.tika.pipes.api.emitter.EmitKey;
@@ -64,7 +63,6 @@ import org.apache.tika.pipes.core.extractor.EmbeddedDocumentBytesConfig;
import org.apache.tika.pipes.core.fetcher.FetcherManager;
import org.apache.tika.pipes.core.serialization.JsonFetchEmitTuple;
import org.apache.tika.plugins.TikaPluginManager;
-import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.serialization.JsonMetadataList;
import org.apache.tika.server.core.CXFTestBase;
import org.apache.tika.server.core.FetcherStreamFactory;
@@ -183,8 +181,9 @@ public class TikaPipesTest extends CXFTestBase {
@Test
public void testConcatenated() throws Exception {
ParseContext parseContext = new ParseContext();
- HandlerConfig handlerConfig = new HandlerConfig(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, HandlerConfig.PARSE_MODE.CONCATENATE, -1, -1, true);
- parseContext.set(HandlerConfig.class, handlerConfig);
+ // Use addConfig with JSON for handler-config
+ parseContext.addConfig("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"),
new EmitKey(EMITTER_JSON_ID, ""), new Metadata(), parseContext,
@@ -192,9 +191,6 @@ public class TikaPipesTest extends CXFTestBase {
StringWriter writer = new StringWriter();
JsonFetchEmitTuple.toJson(t, writer);
- FetchEmitTuple deserialized = JsonFetchEmitTuple.fromJson(new StringReader(writer.toString()));
- assertEquals(t, deserialized);
-
String getUrl = endPoint + PIPES_PATH;
Response response = WebClient
.create(getUrl)
@@ -214,14 +210,12 @@ public class TikaPipesTest extends CXFTestBase {
@Test
public void testPDFConfig() throws Exception {
- Metadata metadata = new Metadata();
ParseContext parseContext = new ParseContext();
- PDFParserConfig pdfParserConfig = new PDFParserConfig();
- pdfParserConfig.setSortByPosition(true);
- parseContext.set(PDFParserConfig.class, pdfParserConfig);
+ // Configure PDFParser via JSON config (pdf-parser is self-configuring)
+ parseContext.addConfig("pdf-parser", "{\"sortByPosition\": true}");
FetchEmitTuple t = new FetchEmitTuple("myId", new FetchKey(FETCHER_ID, TEST_TWO_BOXES_PDF),
- new EmitKey(EMITTER_JSON_ID, ""), metadata, parseContext);
+ new EmitKey(EMITTER_JSON_ID, ""), new Metadata(), parseContext);
StringWriter writer = new StringWriter();
JsonFetchEmitTuple.toJson(t, writer);
String getUrl = endPoint + PIPES_PATH;
diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
index a81e31d994..ee0a68aa0c 100644
--- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
+++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java
@@ -21,6 +21,7 @@ import static org.apache.cxf.helpers.HttpHeaderHelper.CONTENT_ENCODING;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -32,9 +33,7 @@ import java.util.List;
import java.util.Locale;
import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
-import org.apache.commons.codec.binary.Base64;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.cxf.attachment.AttachmentUtil;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
@@ -53,12 +52,8 @@ import org.apache.tika.parser.ocr.TesseractOCRParser;
import org.apache.tika.serialization.JsonMetadata;
import org.apache.tika.server.core.CXFTestBase;
import org.apache.tika.server.core.TikaServerParseExceptionMapper;
-import org.apache.tika.server.core.config.DocumentSelectorConfig;
-import org.apache.tika.server.core.config.PasswordProviderConfig;
import org.apache.tika.server.core.resource.TikaResource;
import org.apache.tika.server.core.writer.JSONMessageBodyWriter;
-import org.apache.tika.server.standard.config.PDFServerConfig;
-import org.apache.tika.server.standard.config.TesseractServerConfig;
public class TikaResourceTest extends CXFTestBase {
public static final String TEST_DOC = "test-documents/test.doc";
@@ -305,14 +300,29 @@ public class TikaResourceTest extends CXFTestBase {
return;
}
+ String configJson = """
+ {
+ "pdf-parser": {
+ "ocrStrategy": "OCR_ONLY"
+ },
+ "tesseract-ocr-parser": {
+ "language": "eng+fra",
+ "minFileSizeToOcr": 10,
+ "maxFileSizeToOcr": 1000000000
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(endPoint + TIKA_PATH)
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "OcrStrategy", "ocr_only")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX + "Language", "eng+fra")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX + "MinFileSizeToOcr", "10")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX + "MaxFileSizeToOcr", "1000000000")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
String responseMsg = getStringFromInputStream((InputStream) response.getEntity());
assertContains("Happy New Year 2003!", responseMsg);
}
@@ -324,97 +334,92 @@ public class TikaResourceTest extends CXFTestBase {
return;
}
- Response response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
- .accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "OcrStrategy", "no_ocr")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- String responseMsg = getStringFromInputStream((InputStream) response.getEntity());
-
- assertEquals("", responseMsg.trim());
-
- response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
- .accept("text/plain")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX + "skipOcr", "true")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- responseMsg = getStringFromInputStream((InputStream) response.getEntity());
-
- assertEquals("", responseMsg.trim());
-
-
- response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
- .accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "OcrStrategy", "ocr_only")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- responseMsg = getStringFromInputStream((InputStream) response.getEntity());
- assertContains("Happy New Year 2003!", responseMsg);
-
- //now try a bad value
- response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
- .accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "OcrStrategy", "non-sense-value")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- assertEquals(400, response.getStatus());
- }
-
- // TIKA-3320
- @Test
- public void testPDFLowerCaseOCRConfig() throws Exception {
- if (!new TesseractOCRParser().hasTesseract()) {
- return;
- }
+ // Test no_ocr strategy
+ String configJson = """
+ {
+ "pdf-parser": {
+ "ocrStrategy": "NO_OCR"
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
Response response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX.toLowerCase(Locale.ROOT) + "ocrstrategy", "no_ocr")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
String responseMsg = getStringFromInputStream((InputStream) response.getEntity());
-
assertEquals("", responseMsg.trim());
+ // Test Tesseract skipOcr via JSON config
+ configJson = "{\"parseContext\": {\"tesseract-ocr-parser\": {\"skipOcr\": true}}}";
+ fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX.toLowerCase(Locale.ROOT) + "skipocr", "true")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
responseMsg = getStringFromInputStream((InputStream) response.getEntity());
-
assertEquals("", responseMsg.trim());
+ // Test ocr_only strategy
+ configJson = """
+ {
+ "pdf-parser": {
+ "ocrStrategy": "OCR_ONLY"
+ }
+ }
+ """;
+ fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX.toLowerCase(Locale.ROOT) + "ocrstrategy", "ocr_only")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
responseMsg = getStringFromInputStream((InputStream) response.getEntity());
assertContains("Happy New Year 2003!", responseMsg);
- //now try a bad value
+ // Test bad value - should return error
+ configJson = """
+ {
+ "pdf-parser": {
+ "ocrStrategy": "non-sense-value"
+ }
+ }
+ """;
+ fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX.toLowerCase(Locale.ROOT) + "ocrstrategy", "non-sense-value")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- assertEquals(400, response.getStatus());
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
+ assertEquals(422, response.getStatus());
}
//TIKA-2669
@Test
public void testPDFConfig() throws Exception {
-
+ // Test default behavior (sortByPosition=true from server config)
Response response = WebClient
.create(endPoint + TIKA_PATH)
.type("application/pdf")
@@ -426,19 +431,33 @@ public class TikaResourceTest extends CXFTestBase {
.trim();
assertEquals("Left column line 1 Right column line 1 Left colu mn line 2 Right column line 2", responseMsg);
+ // Test with sortByPosition=false via JSON config
+ String configJson = """
+ {
+ "pdf-parser": {
+ "sortByPosition": false
+ }
+ }
+ """;
+
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.pdf\"");
+ Attachment fileAtt = new Attachment("file", ClassLoader.getSystemResourceAsStream("test-documents/testPDFTwoTextBoxes.pdf"), fileCd);
+
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "sortByPosition", "false")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testPDFTwoTextBoxes.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
responseMsg = getStringFromInputStream((InputStream) response.getEntity());
responseMsg = responseMsg
.replaceAll("[\r\n ]+", " ")
.trim();
assertEquals("Left column line 1 Left column line 2 Right column line 1 Right column line 2", responseMsg);
- //make sure that default reverts to initial config option
+ // Make sure that default reverts to initial config option
response = WebClient
.create(endPoint + TIKA_PATH)
.type("application/pdf")
@@ -449,7 +468,6 @@ public class TikaResourceTest extends CXFTestBase {
.replaceAll("[\r\n ]+", " ")
.trim();
assertEquals("Left column line 1 Right column line 1 Left colu mn line 2 Right column line 2", responseMsg);
-
}
@@ -471,68 +489,137 @@ public class TikaResourceTest extends CXFTestBase {
}
@Test
- public void testDataIntegrityCheck() {
- Response response;
+ public void testDataIntegrityCheck() throws Exception {
+ // This test requires tesseract to be installed - the validation only happens
+ // when TesseractOCRParser is actually invoked during parsing
+ assumeTrue(new TesseractOCRParser().hasTesseract(), "Tesseract not installed, skipping test");
+
+ // Test bad tesseract path with null byte - should be rejected
+ String configJson = """
+ {
+ "tesseract-ocr-parser": {
+ "tesseractPath": "C://tmp//hello.bat\u0000"
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
try {
- response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ Response response = WebClient
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX + "tesseractPath", "C://tmp//hello.bat\u0000")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- assertEquals(400, response.getStatus());
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
+ assertEquals(500, response.getStatus());
} catch (ProcessingException e) {
//can't tell why this intermittently happens. :(
//started after the upgrade to 3.2.7
}
+ // Test bogus tesseract path - should fail
+ configJson = """
+ {
+ "tesseract-ocr-parser": {
+ "tesseractPath": "bogus path"
+ }
+ }
+ """;
+ fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
try {
- response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ Response response = WebClient
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX + "tesseractPath", "bogus path")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- assertEquals(500, response.getStatus());
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
+ assertEquals(422, response.getStatus());
} catch (ProcessingException e) {
//swallow
}
}
@Test
- public void testTrustedMethodPrevention() {
+ public void testTrustedMethodPrevention() throws Exception {
+ // This test requires tesseract to be installed - the validation only happens
+ // when TesseractOCRParser is actually invoked during parsing
+ assumeTrue(new TesseractOCRParser().hasTesseract(), "Tesseract not installed, skipping test");
+
+ // Trusted methods should not be settable via JSON config
+ String configJson = """
+ {
+ "tesseract-ocr-parser": {
+ "trustedPageSeparator": " "
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(TesseractServerConfig.X_TIKA_OCR_HEADER_PREFIX + "trustedPageSeparator", "\u0020")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
- assertEquals(400, response.getStatus());
-
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
+ assertEquals(422, response.getStatus());
}
@Test
- public void testFloatInHeader() {
+ public void testFloatInConfig() throws Exception {
+ String configJson = """
+ {
+ "pdf-parser": {
+ "averageCharTolerance": 2.0
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(endPoint + TIKA_PATH)
- .type("application/pdf")
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "averageCharTolerance", "2.0")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
assertEquals(200, response.getStatus());
-
}
@Test
public void testUnicodePasswordProtectedSpaces() throws Exception {
//TIKA-2858
final String password = " ";
- final String encoded = new Base64().encodeAsString(password.getBytes(StandardCharsets.UTF_8));
+ String configJson = String.format(Locale.ROOT, """
+ {
+ "simple-password-provider": {
+ "password": "%s"
+ }
+ }
+ """, password);
+
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.pdf\"");
+ Attachment fileAtt = new Attachment("file", ClassLoader.getSystemResourceAsStream("test-documents/testPassword4Spaces.pdf"), fileCd);
+
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(endPoint + TIKA_PATH)
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PasswordProviderConfig.PASSWORD_BASE64_UTF8, encoded)
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testPassword4Spaces.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
String responseMsg = getStringFromInputStream((InputStream) response.getEntity());
assertContains("Just some text.", responseMsg);
}
@@ -541,79 +628,56 @@ public class TikaResourceTest extends CXFTestBase {
public void testUnicodePasswordProtectedUnicode() throws Exception {
//TIKA-2858
final String password = " ! < > \" \\ \u20AC \u0153 \u00A4 \u0031\u2044\u0034 " + "\u0031\u2044\u0032 \uD841\uDF0E \uD867\uDD98 \uD83D\uDE00 ";
- final String encoded = new Base64().encodeAsString(password.getBytes(StandardCharsets.UTF_8));
+ // Escape the password for JSON
+ String escapedPassword = password.replace("\\", "\\\\").replace("\"", "\\\"");
+ String configJson = "{\"simple-password-provider\": {\"password\": \"" + escapedPassword + "\"}}";
+
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.pdf\"");
+ Attachment fileAtt = new Attachment("file", ClassLoader.getSystemResourceAsStream("test-documents/testUnicodePassword.pdf"), fileCd);
+
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(endPoint + TIKA_PATH)
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
.accept("text/plain")
- .header(PasswordProviderConfig.PASSWORD_BASE64_UTF8, encoded)
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testUnicodePassword.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
String responseMsg = getStringFromInputStream((InputStream) response.getEntity());
assertContains("Just some text.", responseMsg);
}
- // TIKA-3227
+ // TIKA-3227 - Skip embedded documents via config
@Test
public void testSkipEmbedded() throws Exception {
+ // First test: without skip-embedded-document-selector, embedded content IS present
Response response = WebClient
.create(endPoint + TIKA_PATH)
.accept("text/plain")
- .header(DocumentSelectorConfig.X_TIKA_SKIP_EMBEDDED_HEADER, "false")
.put(ClassLoader.getSystemResourceAsStream(TEST_RECURSIVE_DOC));
String responseMsg = getStringFromInputStream((InputStream) response.getEntity());
assertContains("embed4.txt", responseMsg);
- response = WebClient
- .create(endPoint + TIKA_PATH)
- .accept("text/plain")
- .header(DocumentSelectorConfig.X_TIKA_SKIP_EMBEDDED_HEADER, "true")
- .put(ClassLoader.getSystemResourceAsStream(TEST_RECURSIVE_DOC));
- responseMsg = getStringFromInputStream((InputStream) response.getEntity());
- assertNotFound("embed4.txt", responseMsg);
- }
-
- // TIKA-3344
- @Test
- public void testPDFLowerCaseOCRConfigPOST() throws Exception {
- if (!new TesseractOCRParser().hasTesseract()) {
- return;
- }
-
- Response response = WebClient
- .create(endPoint + TIKA_POST_PATH)
- .type("application/pdf")
- .accept(MediaType.TEXT_PLAIN)
- .type(MediaType.MULTIPART_FORM_DATA)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX.toLowerCase(Locale.ROOT) + "ocrstrategy", "no_ocr")
- .post(testPDFLowerCaseOCRConfigPOSTBody());
- String responseMsg = getStringFromInputStream((InputStream) response.getEntity());
+ // Second test: with skip-embedded-document-selector, embedded content is NOT present
+ String configJson = """
+ {
+ "skip-embedded-document-selector": {}
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.docx\"");
+ Attachment fileAtt = new Attachment("file", ClassLoader.getSystemResourceAsStream(TEST_RECURSIVE_DOC), fileCd);
- assertEquals("", responseMsg.trim());
+ ContentDisposition configCd = new ContentDisposition("form-data; name=\"config\"; filename=\"config.json\"");
+ Attachment configAtt = new Attachment("config", "application/json",
+ new java.io.ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
response = WebClient
- .create(endPoint + TIKA_POST_PATH)
- .type("application/pdf")
- .accept(MediaType.TEXT_PLAIN)
- .type(MediaType.MULTIPART_FORM_DATA)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX.toLowerCase(Locale.ROOT) + "ocrstrategy", "ocr_only")
- .post(testPDFLowerCaseOCRConfigPOSTBody());
+ .create(endPoint + TIKA_PATH + "/config")
+ .type("multipart/form-data")
+ .accept("text/plain")
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
responseMsg = getStringFromInputStream((InputStream) response.getEntity());
- assertContains("Happy New Year 2003!", responseMsg);
-
- //now try a bad value
- response = WebClient
- .create(endPoint + TIKA_POST_PATH)
- .type("application/pdf")
- .accept(MediaType.TEXT_PLAIN)
- .type(MediaType.MULTIPART_FORM_DATA)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX.toLowerCase(Locale.ROOT) + "ocrstrategy", "non-sense-value")
- .post(testPDFLowerCaseOCRConfigPOSTBody());
- assertEquals(400, response.getStatus());
- }
-
- private MultipartBody testPDFLowerCaseOCRConfigPOSTBody() {
- ContentDisposition cd = new ContentDisposition("form-data; name=\"input\"; filename=\"testOCR.pdf\"");
- Attachment att = new Attachment("upload", ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), cd);
- return new MultipartBody(att);
+ assertNotFound("embed4.txt", responseMsg);
}
@Test
diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceTest.java
index 9929056786..84def0eedf 100644
--- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceTest.java
+++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceTest.java
@@ -25,7 +25,9 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
@@ -34,6 +36,9 @@ import jakarta.ws.rs.core.Response;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.junit.jupiter.api.Test;
@@ -45,7 +50,6 @@ import org.apache.tika.server.core.TikaServerParseExceptionMapper;
import org.apache.tika.server.core.resource.UnpackerResource;
import org.apache.tika.server.core.writer.TarWriter;
import org.apache.tika.server.core.writer.ZipWriter;
-import org.apache.tika.server.standard.config.PDFServerConfig;
public class UnpackerResourceTest extends CXFTestBase {
private static final String BASE_PATH = "/unpack";
@@ -158,6 +162,7 @@ public class UnpackerResourceTest extends CXFTestBase {
@Test
public void test204() throws Exception {
+ //this tests that the type overrides normal detection
Response response = WebClient
.create(CXFTestBase.endPoint + UNPACKER_PATH)
.type("xxx/xxx")
@@ -233,11 +238,24 @@ public class UnpackerResourceTest extends CXFTestBase {
@Test
public void testPDFImages() throws Exception {
+ String configJson = """
+ {
+ "pdf-parser": {
+ "extractInlineImages": true
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(CXFTestBase.endPoint + UNPACKER_PATH)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "ExtractInlineImages", "true")
+ .create(CXFTestBase.endPoint + UNPACKER_PATH + "/config")
+ .type("multipart/form-data")
.accept("application/zip")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
Map<String, String> results = readZipArchive((InputStream) response.getEntity());
assertTrue(results.containsKey("image0.png"));
String md5 = results.get("image0.png");
@@ -252,24 +270,49 @@ public class UnpackerResourceTest extends CXFTestBase {
public void testPDFRenderOCR() throws Exception {
assumeTrue(new TesseractOCRParser().hasTesseract());
+ String configJson = """
+ {
+ "pdf-parser": {
+ "ocrStrategy": "OCR_ONLY"
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testOCR.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(CXFTestBase.endPoint + ALL_PATH)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "ocrStrategy", "ocr_only")
+ .create(CXFTestBase.endPoint + ALL_PATH + "/config")
+ .type("multipart/form-data")
.accept("application/zip")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testOCR.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
String txt = readArchiveText((InputStream) response.getEntity());
CXFTestBase.assertContains("Happy New Year", txt);
}
@Test
public void testPDFPerPageRenderColor() throws Exception {
+ String configJson = """
+ {
+ "pdf-parser": {
+ "imageStrategy": "RENDER_PAGES_AT_PAGE_END",
+ "ocrImageType": "RGB"
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testColorRendering.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testColorRendering.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
Response response = WebClient
- .create(CXFTestBase.endPoint + ALL_PATH)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "imageStrategy", "RenderPagesAtPageEnd")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "ocrImageType", "rgb")
+ .create(CXFTestBase.endPoint + ALL_PATH + "/config")
+ .type("multipart/form-data")
.accept("application/zip")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testColorRendering.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
Map<String, byte[]> results = readZipArchiveBytes((InputStream) response.getEntity());
byte[] renderedImage = null;
for (Map.Entry<String, byte[]> e : results.entrySet()) {
diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceWithConfigTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceWithConfigTest.java
index f6f2b2bd24..e961d1ccc7 100644
--- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceWithConfigTest.java
+++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/UnpackerResourceWithConfigTest.java
@@ -23,7 +23,9 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
@@ -31,6 +33,9 @@ import javax.imageio.ImageIO;
import jakarta.ws.rs.core.Response;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.junit.jupiter.api.Test;
@@ -41,7 +46,6 @@ import org.apache.tika.server.core.TikaServerParseExceptionMapper;
import org.apache.tika.server.core.resource.UnpackerResource;
import org.apache.tika.server.core.writer.TarWriter;
import org.apache.tika.server.core.writer.ZipWriter;
-import org.apache.tika.server.standard.config.PDFServerConfig;
public class UnpackerResourceWithConfigTest extends CXFTestBase {
private static final String BASE_PATH = "/unpack";
@@ -73,15 +77,27 @@ public class UnpackerResourceWithConfigTest extends CXFTestBase {
//when specified in tika-config.json
@Test
public void testPDFPerPageRenderColor() throws Exception {
-
//default is gray scale png; change to rgb and tiff
+ String configJson = """
+ {
+ "pdf-parser": {
+ "imageStrategy": "RENDER_PAGES_AT_PAGE_END",
+ "ocrImageType": "RGB",
+ "ocrImageFormatName": "tiff"
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testColorRendering.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testColorRendering.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
+
Response response = WebClient
- .create(CXFTestBase.endPoint + ALL_PATH)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "imageStrategy", "RenderPagesAtPageEnd")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "ocrImageType", "rgb")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "ocrImageFormatName", "tiff")
+ .create(CXFTestBase.endPoint + ALL_PATH + "/config")
+ .type("multipart/form-data")
.accept("application/zip")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testColorRendering.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
Map<String, byte[]> results = readZipArchiveBytes((InputStream) response.getEntity());
byte[] renderedImage = null;
for (Map.Entry<String, byte[]> e : results.entrySet()) {
@@ -125,15 +141,26 @@ public class UnpackerResourceWithConfigTest extends CXFTestBase {
@Test
public void testPDFPerPageRenderGray() throws Exception {
-
+ String configJson = """
+ {
+ "pdf-parser": {
+ "imageStrategy": "RENDER_PAGES_AT_PAGE_END",
+ "ocrImageType": "GRAY",
+ "ocrImageFormatName": "jpeg"
+ }
+ }
+ """;
+ ContentDisposition fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"testColorRendering.pdf\"");
+ Attachment fileAtt = new Attachment("file",
+ ClassLoader.getSystemResourceAsStream("test-documents/testColorRendering.pdf"), fileCd);
+ Attachment configAtt = new Attachment("config", "application/json",
+ new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)));
Response response = WebClient
- .create(CXFTestBase.endPoint + ALL_PATH)
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "imageStrategy", "RenderPagesAtPageEnd")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "ocrImageType", "gray")
- .header(PDFServerConfig.X_TIKA_PDF_HEADER_PREFIX + "ocrImageFormatName", "jpeg")
+ .create(CXFTestBase.endPoint + ALL_PATH + "/config")
+ .type("multipart/form-data")
.accept("application/zip")
- .put(ClassLoader.getSystemResourceAsStream("test-documents/testColorRendering.pdf"));
+ .post(new MultipartBody(Arrays.asList(fileAtt, configAtt)));
Map<String, byte[]> results = readZipArchiveBytes((InputStream) response.getEntity());
byte[] renderedImage = null;
for (Map.Entry<String, byte[]> e : results.entrySet()) {