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 <[email protected]> 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
