Skip to content

Commit 9eebc5a

Browse files
authored
TIKA-4562 -- refactor runtime configs in tika-server to json
1 parent 05c286c commit 9eebc5a

72 files changed

Lines changed: 2118 additions & 1382 deletions

File tree

Some content is hidden

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

tika-annotation-processor/src/main/java/org/apache/tika/annotation/TikaComponentProcessor.java

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
import javax.annotation.processing.SupportedAnnotationTypes;
3535
import javax.annotation.processing.SupportedSourceVersion;
3636
import javax.lang.model.SourceVersion;
37+
import javax.lang.model.element.AnnotationMirror;
38+
import javax.lang.model.element.AnnotationValue;
3739
import javax.lang.model.element.Element;
40+
import javax.lang.model.element.ExecutableElement;
3841
import javax.lang.model.element.TypeElement;
3942
import javax.lang.model.type.DeclaredType;
4043
import javax.lang.model.type.TypeMirror;
@@ -125,17 +128,31 @@ private void processComponent(TypeElement element) {
125128
// Check if component should be included in SPI
126129
boolean includeSpi = annotation.spi();
127130

131+
// Get contextKey if specified (need to use mirror API for Class types)
132+
String contextKey = getContextKeyFromAnnotation(element);
133+
128134
messager.printMessage(Diagnostic.Kind.NOTE,
129135
"Processing @TikaComponent: " + className + " -> " + componentName +
130-
" (SPI: " + includeSpi + ")");
136+
" (SPI: " + includeSpi + ", contextKey: " + contextKey + ")");
131137

132138
// Find all implemented service interfaces
133139
List<String> serviceInterfaces = findServiceInterfaces(element);
134140

141+
// Build the index entry value (className or className:key=X)
142+
String indexValue = className;
143+
if (contextKey != null) {
144+
indexValue = className + ":key=" + contextKey;
145+
}
146+
135147
if (serviceInterfaces.isEmpty()) {
136-
messager.printMessage(Diagnostic.Kind.WARNING,
137-
"Class " + className + " annotated with @TikaComponent " +
138-
"but does not implement any known Tika service interface", element);
148+
// No known service interface - put in other-configs.idx
149+
messager.printMessage(Diagnostic.Kind.NOTE,
150+
"Class " + className + " does not implement known service interface, " +
151+
"adding to other-configs.idx", element);
152+
153+
Map<String, String> index = indexFiles.computeIfAbsent("other-configs",
154+
k -> new LinkedHashMap<>());
155+
addToIndex(index, componentName, indexValue, className, element);
139156
return;
140157
}
141158

@@ -152,20 +169,57 @@ private void processComponent(TypeElement element) {
152169
if (indexFileName != null) {
153170
Map<String, String> index = indexFiles.computeIfAbsent(indexFileName,
154171
k -> new LinkedHashMap<>());
172+
addToIndex(index, componentName, indexValue, className, element);
173+
}
174+
}
175+
}
155176

156-
// Check for duplicate names
157-
if (index.containsKey(componentName)) {
158-
String existingClass = index.get(componentName);
159-
if (!existingClass.equals(className)) {
160-
messager.printMessage(Diagnostic.Kind.ERROR,
161-
"Duplicate component name '" + componentName + "' for classes: " +
162-
existingClass + " and " + className, element);
177+
/**
178+
* Adds an entry to an index, checking for duplicates.
179+
*/
180+
private void addToIndex(Map<String, String> index, String componentName,
181+
String indexValue, String className, TypeElement element) {
182+
if (index.containsKey(componentName)) {
183+
String existingValue = index.get(componentName);
184+
// Extract class name from value (may have :key= suffix)
185+
String existingClass = existingValue.contains(":")
186+
? existingValue.substring(0, existingValue.indexOf(":"))
187+
: existingValue;
188+
if (!existingClass.equals(className)) {
189+
messager.printMessage(Diagnostic.Kind.ERROR,
190+
"Duplicate component name '" + componentName + "' for classes: " +
191+
existingClass + " and " + className, element);
192+
}
193+
} else {
194+
index.put(componentName, indexValue);
195+
}
196+
}
197+
198+
/**
199+
* Gets the contextKey value from the annotation using the mirror API.
200+
* Returns null if contextKey is void.class (the default).
201+
*/
202+
private String getContextKeyFromAnnotation(TypeElement element) {
203+
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
204+
DeclaredType annotationType = mirror.getAnnotationType();
205+
if (annotationType.toString().equals(TikaComponent.class.getName())) {
206+
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry
207+
: mirror.getElementValues().entrySet()) {
208+
if (entry.getKey().getSimpleName().toString().equals("contextKey")) {
209+
// The value is a TypeMirror for Class types
210+
Object value = entry.getValue().getValue();
211+
if (value instanceof TypeMirror) {
212+
String typeName = value.toString();
213+
// void.class is the default, meaning "auto-detect"
214+
if (!"void".equals(typeName) && !"java.lang.Void".equals(typeName)) {
215+
return typeName;
216+
}
217+
}
163218
}
164-
} else {
165-
index.put(componentName, className);
166219
}
167220
}
168221
}
222+
return null;
169223
}
170224

171225
/**
@@ -267,7 +321,7 @@ private void writeIndexFiles() {
267321
writeApacheLicenseHeader(writer);
268322
writer.write("# Generated by TikaComponentProcessor\n");
269323
writer.write("# Do not edit manually\n");
270-
writer.write("# Format: component-name=fully.qualified.ClassName\n");
324+
writer.write("# Format: component-name=fully.qualified.ClassName[:key=contextKeyClass]\n");
271325
for (Map.Entry<String, String> component : components.entrySet()) {
272326
writer.write(component.getKey());
273327
writer.write("=");

tika-annotation-processor/src/main/java/org/apache/tika/config/TikaComponent.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
* public class DWGReadParser extends AbstractParser {
5454
* // available by name, but NOT auto-loaded by default-parser
5555
* }
56+
*
57+
* {@code @TikaComponent(contextKey = MetadataFilter.class)}
58+
* public class MyFilter implements MetadataFilter, AnotherInterface {
59+
* // explicit ParseContext key when class implements multiple known interfaces
60+
* }
5661
* </pre>
5762
*
5863
* @since 3.1.0
@@ -81,4 +86,24 @@
8186
* @return true to include in SPI (default), false to require explicit config
8287
*/
8388
boolean spi() default true;
89+
90+
/**
91+
* The class to use as the key when adding this component to ParseContext.
92+
* <p>
93+
* By default ({@code void.class}), the key is auto-detected:
94+
* <ul>
95+
* <li>If the component implements a known interface (e.g., MetadataFilter),
96+
* that interface is used as the key</li>
97+
* <li>Otherwise, the component's own class is used as the key</li>
98+
* </ul>
99+
* <p>
100+
* Use this attribute to explicitly specify the key when:
101+
* <ul>
102+
* <li>The component implements multiple known interfaces (ambiguous)</li>
103+
* <li>You need a specific interface/class that isn't auto-detected</li>
104+
* </ul>
105+
*
106+
* @return the class to use as ParseContext key, or void.class for auto-detection
107+
*/
108+
Class<?> contextKey() default void.class;
84109
}

tika-app/src/main/java/org/apache/tika/cli/XmlToJsonConfigConverter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.w3c.dom.Node;
4141
import org.w3c.dom.NodeList;
4242

43+
import org.apache.tika.config.loader.ComponentInfo;
4344
import org.apache.tika.config.loader.ComponentRegistry;
4445
import org.apache.tika.config.loader.KebabCaseConverter;
4546
import org.apache.tika.exception.TikaConfigException;
@@ -585,8 +586,8 @@ private static String classNameToComponentName(String fullClassName, ComponentRe
585586
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(fullClassName);
586587

587588
// Reverse lookup: find the component name for this class
588-
for (Map.Entry<String, Class<?>> entry : registry.getAllComponents().entrySet()) {
589-
if (entry.getValue().equals(clazz)) {
589+
for (Map.Entry<String, ComponentInfo> entry : registry.getAllComponents().entrySet()) {
590+
if (entry.getValue().componentClass().equals(clazz)) {
590591
return entry.getKey();
591592
}
592593
}

tika-core/src/main/java/org/apache/tika/config/ConfigContainer.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.tika.config;
1818

19+
import java.io.Serializable;
1920
import java.util.Collections;
2021
import java.util.HashMap;
2122
import java.util.Map;
@@ -32,7 +33,9 @@
3233
* and other components to look up their config by friendly name (e.g., "pdf-parser",
3334
* "fs-fetcher-1") and deserialize it on-demand.
3435
*/
35-
public class ConfigContainer {
36+
public class ConfigContainer implements Serializable {
37+
38+
private static final long serialVersionUID = 1L;
3639

3740
private final Map<String, String> configs = new HashMap<>();
3841

@@ -83,4 +86,14 @@ public Set<String> getKeys() {
8386
public boolean isEmpty() {
8487
return configs.isEmpty();
8588
}
89+
90+
/**
91+
* Removes the configuration with the specified key.
92+
*
93+
* @param key the friendly name of the config to remove
94+
* @return the previous value associated with the key, or null if there was no mapping
95+
*/
96+
public String remove(String key) {
97+
return configs.remove(key);
98+
}
8699
}

tika-core/src/main/java/org/apache/tika/config/ParseContextConfig.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,19 @@ public class ParseContextConfig {
6767
}
6868

6969
/**
70-
* Retrieves runtime configuration from ParseContext's ConfigContainer.
70+
* Retrieves runtime configuration from ParseContext.
71+
* <p>
72+
* This method first checks if the config is already resolved in ParseContext
73+
* (via {@code context.get(configClass)}). If found, it returns immediately without
74+
* re-deserializing. This is efficient for embedded documents where the config
75+
* was already deserialized for the parent document.
76+
* <p>
77+
* If not found, it checks ConfigContainer for the config key and deserializes
78+
* the JSON. The deserialized config is also set in ParseContext for future lookups.
7179
* <p>
7280
* This method performs defensive checking: if the ConfigContainer has configuration
7381
* for the requested key but the ConfigDeserializer is not available on the classpath,
74-
* it throws IllegalStateException. This prevents silent failures where users expect
75-
* their runtime config to be used but it's silently ignored.
82+
* it throws TikaConfigException to prevent silent failures.
7683
*
7784
* @param context the parse context (may be null)
7885
* @param configKey the configuration key (e.g., "pdf-parser", "html-parser")
@@ -90,6 +97,13 @@ public static <T> T getConfig(ParseContext context, String configKey,
9097
return defaultConfig;
9198
}
9299

100+
// First check if config is already resolved in ParseContext
101+
// (may have been set by a previous call or by user code)
102+
T existingConfig = context.get(configClass);
103+
if (existingConfig != null) {
104+
return existingConfig;
105+
}
106+
93107
ConfigContainer configContainer = context.get(ConfigContainer.class);
94108
if (configContainer == null) {
95109
return defaultConfig;
@@ -113,6 +127,7 @@ public static <T> T getConfig(ParseContext context, String configKey,
113127
}
114128

115129
// ConfigDeserializer is available - delegate to it
130+
// (ConfigDeserializer.getConfig also sets the config in ParseContext for future lookups)
116131
try {
117132
@SuppressWarnings("unchecked")
118133
T result = (T) GET_CONFIG_METHOD.invoke(null, context, configKey, configClass, defaultConfig);
@@ -122,7 +137,7 @@ public static <T> T getConfig(ParseContext context, String configKey,
122137
if (cause instanceof IOException) {
123138
throw (IOException) cause;
124139
}
125-
throw new IOException("Failed to deserialize config for '" + configKey + "': " +
140+
throw new IOException("Failed to deserialize config for '" + configKey + "': " +
126141
cause.getMessage(), cause);
127142
}
128143
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.config;
18+
19+
/**
20+
* Marker interface indicating that a component reads its own configuration
21+
* from {@link ConfigContainer} at runtime.
22+
* <p>
23+
* Components implementing this interface will NOT be automatically resolved
24+
* by ParseContextUtils. Instead, the JSON configuration will remain in
25+
* ConfigContainer, and the component is responsible for reading and applying
26+
* its own configuration during execution.
27+
* <p>
28+
* This is typically used by parsers and other components that need fine-grained
29+
* control over how their configuration is loaded and merged with defaults.
30+
* <p>
31+
* Example:
32+
* <pre>
33+
* {@literal @}TikaComponent
34+
* public class PDFParser implements Parser, SelfConfiguring {
35+
*
36+
* private final PDFParserConfig defaultConfig;
37+
*
38+
* public void parse(..., ParseContext context) {
39+
* // Component reads its own config from ConfigContainer
40+
* PDFParserConfig config = ParseContextConfig.getConfig(
41+
* context, "pdf-parser", PDFParserConfig.class, defaultConfig);
42+
* // use config...
43+
* }
44+
* }
45+
* </pre>
46+
* <p>
47+
* Components that do NOT implement this interface will have their configuration
48+
* automatically deserialized and added to ParseContext by ParseContextUtils.
49+
*
50+
* @since Apache Tika 4.0
51+
* @see ConfigContainer
52+
* @see ParseContextConfig
53+
*/
54+
public interface SelfConfiguring {
55+
// Marker interface - no methods
56+
}

tika-core/src/main/java/org/apache/tika/config/TikaTaskTimeout.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,34 @@
2020

2121
import org.apache.tika.parser.ParseContext;
2222

23+
/**
24+
* Configuration class for specifying parse task timeout.
25+
* <pre>
26+
* {
27+
* "parse-context": {
28+
* "tika-task-timeout": {
29+
* "timeoutMillis": 30000
30+
* }
31+
* }
32+
* }
33+
* </pre>
34+
*/
35+
@TikaComponent(spi = false)
2336
public class TikaTaskTimeout implements Serializable {
2437

25-
private final long timeoutMillis;
38+
private long timeoutMillis;
39+
40+
/**
41+
* No-arg constructor for Jackson deserialization.
42+
*/
43+
public TikaTaskTimeout() {
44+
}
2645

46+
/**
47+
* Constructor with timeout value.
48+
*
49+
* @param timeoutMillis timeout in milliseconds
50+
*/
2751
public TikaTaskTimeout(long timeoutMillis) {
2852
this.timeoutMillis = timeoutMillis;
2953
}
@@ -32,6 +56,10 @@ public long getTimeoutMillis() {
3256
return timeoutMillis;
3357
}
3458

59+
public void setTimeoutMillis(long timeoutMillis) {
60+
this.timeoutMillis = timeoutMillis;
61+
}
62+
3563
public static long getTimeoutMillis(ParseContext context, long defaultTimeoutMillis) {
3664
if (context == null) {
3765
return defaultTimeoutMillis;

0 commit comments

Comments
 (0)