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 2026/08/07 20:07:57 UTC
(tika) 06/09: TIKA-4809: Migrate /meta onto the shared pipes-backed PipesParser
This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch TIKA-4809-stage-2
in repository https://gitbox.apache.org/repos/asf/tika.git
commit 519963cdbcac734cf50d008085e5e1319cbba8cd
Author: tallison <ta...@apache.org>
AuthorDate: Fri Aug 7 15:18:34 2026 -0400
TIKA-4809: Migrate /meta onto the shared pipes-backed PipesParser
---
.../server/core/resource/MetadataResource.java | 107 ++++++++++-----------
.../apache/tika/server/core/StackTraceTest.java | 13 ++-
.../standard/resource/XMPMetadataResource.java | 6 +-
.../tika/server/standard/MetadataResourceTest.java | 24 +++--
4 files changed, 76 insertions(+), 74 deletions(-)
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 ec0d9bc46f..1fbc0bffdb 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
@@ -18,7 +18,6 @@ package org.apache.tika.server.core.resource;
import static org.apache.tika.server.core.resource.TikaResource.fillMetadata;
-import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@@ -37,13 +36,16 @@ import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.tika.exception.TikaConfigException;
-import org.apache.tika.extractor.DocumentSelector;
+import org.apache.tika.config.EmbeddedLimits;
+import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
-import org.apache.tika.language.detect.LanguageHandler;
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.pipes.api.ParseMode;
+import org.apache.tika.sax.BasicContentHandlerFactory;
+import org.apache.tika.sax.ContentHandlerFactory;
+import org.apache.tika.server.core.TikaServerParseException;
@Path("/meta")
@@ -63,8 +65,9 @@ public class MetadataResource {
public Response getMetadataFromMultipart(Attachment att, @Context UriInfo info) throws Exception {
ParseContext context = tikaResource.createParseContext();
try (TikaInputStream tis = TikaInputStream.get(att.getObject(InputStream.class))) {
+ tis.getPath(); // Spool to temp file for pipes-based parsing
return Response
- .ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), info))
+ .ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), context))
.build();
}
}
@@ -79,25 +82,14 @@ public class MetadataResource {
@Path("config")
public Response getMetadataWithConfig(
List<Attachment> attachments,
- @Context HttpHeaders httpHeaders,
- @Context UriInfo info) throws Exception {
+ @Context HttpHeaders httpHeaders) throws Exception {
// Load default context from config, then overlay with request config
ParseContext context = tikaResource.createParseContext();
Metadata metadata = Metadata.newInstance(context);
try (TikaInputStream tis = tikaResource.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();
+ return Response.ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), context)).build();
}
}
@@ -107,19 +99,19 @@ public class MetadataResource {
ParseContext context = tikaResource.createParseContext();
Metadata metadata = Metadata.newInstance(context);
try (TikaInputStream tis = TikaInputStream.get(is)) {
+ tis.getPath(); // Spool to temp file for pipes-based parsing
return Response
- .ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), info))
+ .ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), context))
.build();
}
}
/**
- * Get a specific metadata field. If the input stream cannot be parsed, but a
- * value was found for the given metadata field, then the value of the field
- * is returned as part of a 200 OK response; otherwise a
- * {@link javax.ws.rs.core.Response.Status#BAD_REQUEST} is generated. If the stream
- * was successfully parsed but the specific metadata field was not found, then a
- * {@link javax.ws.rs.core.Response.Status#NOT_FOUND} is returned.
+ * Get a specific metadata field. If the document parses successfully but the
+ * specific metadata field was not found, a
+ * {@link javax.ws.rs.core.Response.Status#NOT_FOUND} is returned. Unlike the other
+ * /meta endpoints, a bare field value has no envelope to embed a container-level
+ * exception in, so that case is thrown (422) instead.
* <p/>
* Note that this method handles multivalue fields and returns possibly more
* metadata value than requested.
@@ -131,35 +123,29 @@ public class MetadataResource {
* @param httpHeaders httpheaders
* @param info info
* @param field the tika metadata field name
- * @return one of {@link javax.ws.rs.core.Response.Status#OK},
- * {@link javax.ws.rs.core.Response.Status#NOT_FOUND}, or
- * {@link javax.ws.rs.core.Response.Status#BAD_REQUEST}
+ * @return one of {@link javax.ws.rs.core.Response.Status#OK} or
+ * {@link javax.ws.rs.core.Response.Status#NOT_FOUND}
* @throws Exception
*/
@PUT
@Path("{field}")
@Produces({"text/csv", "application/json", "text/plain"})
public Response getMetadataField(InputStream is, @Context HttpHeaders httpHeaders, @Context UriInfo info, @PathParam("field") String field) throws Exception {
-
- // use BAD request to indicate that we may not have had enough data to
- // process the request
- Response.Status defaultErrorResponse = Response.Status.BAD_REQUEST;
ParseContext context = tikaResource.createParseContext();
- Metadata metadata = Metadata.newInstance(context);
- boolean success = false;
+ Metadata metadata;
try (TikaInputStream tis = TikaInputStream.get(is)) {
- parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), info);
- // once we've parsed the document successfully, we should use NOT_FOUND
- // if we did not see the field
- defaultErrorResponse = Response.Status.NOT_FOUND;
- success = true;
- } catch (Exception e) {
- LOG.warn("Failed to process field {}", field, e);
+ tis.getPath(); // Spool to temp file for pipes-based parsing
+ metadata = parseMetadata(tis, Metadata.newInstance(context), httpHeaders.getRequestHeaders(), context);
+ }
+
+ String containerException = metadata.get(TikaCoreProperties.CONTAINER_EXCEPTION);
+ if (containerException != null && !containerException.isEmpty()) {
+ throw new TikaServerParseException(new TikaException(containerException));
}
- if (success == false || metadata.get(field) == null) {
+ if (metadata.get(field) == null) {
return Response
- .status(defaultErrorResponse)
+ .status(Response.Status.NOT_FOUND)
.entity("Failed to get metadata field " + field)
.build();
}
@@ -175,21 +161,26 @@ public class MetadataResource {
.build();
}
- protected Metadata parseMetadata(TikaInputStream tis, Metadata metadata, MultivaluedMap<String, String> httpHeaders, UriInfo info)
- throws IOException, TikaConfigException {
- // Load default context from config (includes DigesterFactory from parse-context)
- final ParseContext context = tikaResource.createParseContext();
- Parser parser = tikaResource.createParser();
- fillMetadata(parser, metadata, httpHeaders);
- //no need to parse embedded docs
- context.set(DocumentSelector.class, metadata1 -> false);
+ /**
+ * Parses via the shared pipes-backed PipesParser, stopping at the container document
+ * (EmbeddedLimits maxDepth=0) with content capture off ("ignore" handler) -- metadata
+ * only, matching /meta's contract. Set unconditionally so per-request config can't
+ * turn content capture back on. A container-level exception is embedded in
+ * CONTAINER_EXCEPTION here, not thrown; getMetadataField throws instead since it
+ * returns a bare scalar with nowhere to embed it.
+ */
+ protected Metadata parseMetadata(TikaInputStream tis, Metadata metadata, MultivaluedMap<String, String> httpHeaders, ParseContext context)
+ throws Exception {
+ fillMetadata(null, metadata, httpHeaders);
+ context.set(EmbeddedLimits.class, new EmbeddedLimits(0, false, EmbeddedLimits.UNLIMITED, false));
+ context.set(ContentHandlerFactory.class,
+ new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.IGNORE, -1));
TikaResource.logRequest(LOG, "/meta", metadata);
- tikaResource.parse(parser, LOG, info.getPath(), tis, new LanguageHandler() {
- public void endDocument() {
- metadata.set("language", getLanguage().getLanguage());
- }
- }, metadata, context);
- return metadata;
+ List<Metadata> metadataList = tikaResource.parseWithPipes(tis, metadata, context, ParseMode.RMETA);
+ if (metadataList.isEmpty()) {
+ return Metadata.newInstance(context);
+ }
+ return metadataList.get(0);
}
}
diff --git a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/StackTraceTest.java b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/StackTraceTest.java
index c1aa7b7248..618c019734 100644
--- a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/StackTraceTest.java
+++ b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/StackTraceTest.java
@@ -113,7 +113,8 @@ public class StackTraceTest extends CXFTestBase {
@Test
public void testEncrypted() throws Exception {
for (String path : PATHS) {
- if ("/rmeta".equals(path)) {
+ // /rmeta and /meta embed a container exception at 200 instead of throwing 422.
+ if ("/rmeta".equals(path) || "/meta".equals(path)) {
continue;
}
// Use path-based routing for /tika
@@ -132,7 +133,8 @@ public class StackTraceTest extends CXFTestBase {
@Test
public void testNullPointerOnTika() throws Exception {
for (String path : PATHS) {
- if ("/rmeta".equals(path)) {
+ // Same as testEncrypted.
+ if ("/rmeta".equals(path) || "/meta".equals(path)) {
continue;
}
// Use path-based routing for /tika
@@ -170,10 +172,7 @@ public class StackTraceTest extends CXFTestBase {
}
- //For now, make sure that non-complete document
- //still returns BAD_REQUEST. We may want to
- //make MetadataResource return the same types of parse
- //exceptions as the others...
+ // A truncated document isn't a process failure -- NOT_FOUND, not BAD_REQUEST.
@Test
public void testMeta() throws Exception {
InputStream stream = ClassLoader.getSystemResourceAsStream(TEST_HELLO_WORLD);
@@ -183,7 +182,7 @@ public class StackTraceTest extends CXFTestBase {
.type("application/mock+xml")
.accept(MediaType.TEXT_PLAIN)
.put(copy(stream, 100));
- assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
String msg = getStringFromInputStream((InputStream) response.getEntity());
assertEquals("Failed to get metadata field Author", msg);
}
diff --git a/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/resource/XMPMetadataResource.java b/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/resource/XMPMetadataResource.java
index 3fc7b4501b..6bf07562ef 100644
--- a/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/resource/XMPMetadataResource.java
+++ b/tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/resource/XMPMetadataResource.java
@@ -58,8 +58,9 @@ public class XMPMetadataResource extends MetadataResource implements TikaServerR
public Response getMetadataFromMultipart(Attachment att, @Context UriInfo info) throws Exception {
ParseContext context = new ParseContext();
try (TikaInputStream tis = TikaInputStream.get(att.getObject(InputStream.class))) {
+ tis.getPath(); // Spool to temp file for pipes-based parsing
return Response
- .ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), info))
+ .ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), context))
.build();
}
}
@@ -70,8 +71,9 @@ public class XMPMetadataResource extends MetadataResource implements TikaServerR
ParseContext context = new ParseContext();
Metadata metadata = Metadata.newInstance(context);
try (TikaInputStream tis = TikaInputStream.get(is)) {
+ tis.getPath(); // Spool to temp file for pipes-based parsing
return Response
- .ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), info))
+ .ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), context))
.build();
}
}
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 6e205486ee..e548f9d5c6 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
@@ -47,6 +47,7 @@ import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
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.resource.MetadataResource;
import org.apache.tika.server.core.writer.CSVMessageBodyWriter;
import org.apache.tika.server.core.writer.JSONMessageBodyWriter;
@@ -73,6 +74,8 @@ public class MetadataResourceTest extends CXFTestBase {
@Override
protected void setUpProviders(JAXRSServerFactoryBean sf) {
List<Object> providers = new ArrayList<>();
+ // Needed by getMetadataField's TikaServerParseException throw.
+ providers.add(new TikaServerParseExceptionMapper(false));
providers.add(new JSONMessageBodyWriter());
providers.add(new CSVMessageBodyWriter());
providers.add(new XMPMessageBodyWriter());
@@ -118,10 +121,13 @@ public class MetadataResourceTest extends CXFTestBase {
.accept("application/json")
.post(new MultipartBody(Arrays.asList(fileAtt)));
- // Won't work, no password given - EncryptedDocumentException returns 422
- assertEquals(500, response.getStatus());
+ // A failed decrypt isn't a process failure -- 200, exception on the metadata.
+ assertEquals(200, response.getStatus());
+ Metadata noPasswordMetadata = JsonMetadata.fromJson(new InputStreamReader((InputStream) response.getEntity(), UTF_8));
+ assertContains("org.apache.tika.exception.EncryptedDocumentException",
+ noPasswordMetadata.get(TikaCoreProperties.CONTAINER_EXCEPTION));
- // Test 2: Wrong password - should fail
+ // Test 2: Wrong password - should fail the same way
fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.xls\"");
fileAtt = new Attachment("file",
ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED), fileCd);
@@ -142,7 +148,10 @@ public class MetadataResourceTest extends CXFTestBase {
.accept("application/json")
.post(new MultipartBody(Arrays.asList(fileAtt, wrongConfigAtt)));
- assertEquals(500, response.getStatus());
+ assertEquals(200, response.getStatus());
+ Metadata wrongPasswordMetadata = JsonMetadata.fromJson(new InputStreamReader((InputStream) response.getEntity(), UTF_8));
+ assertContains("org.apache.tika.exception.EncryptedDocumentException",
+ wrongPasswordMetadata.get(TikaCoreProperties.CONTAINER_EXCEPTION));
// Test 3: Correct password - should work
fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.xls\"");
@@ -213,8 +222,9 @@ public class MetadataResourceTest extends CXFTestBase {
}
@Test
- public void testGetField_Author_TEXT_Partial_BAD_REQUEST() throws Exception {
-
+ public void testGetField_Author_TEXT_Partial_UNPROCESSABLE() throws Exception {
+ // Truncating at 8000 bytes corrupts the OLE2 structure enough that OfficeParser
+ // throws -- a real container exception, not just a missing field.
InputStream stream = ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_DOC);
Response response = WebClient
@@ -222,7 +232,7 @@ public class MetadataResourceTest extends CXFTestBase {
.type("application/msword")
.accept(MediaType.TEXT_PLAIN)
.put(copy(stream, 8000));
- assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ assertEquals(422, response.getStatus());
}
@Test