Skip to content

Commit 519963c

Browse files
committed
TIKA-4809: Migrate /meta onto the shared pipes-backed PipesParser
1 parent 391c548 commit 519963c

4 files changed

Lines changed: 76 additions & 74 deletions

File tree

tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/MetadataResource.java

Lines changed: 49 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import static org.apache.tika.server.core.resource.TikaResource.fillMetadata;
2020

21-
import java.io.IOException;
2221
import java.io.InputStream;
2322
import java.util.List;
2423

@@ -37,13 +36,16 @@
3736
import org.slf4j.Logger;
3837
import org.slf4j.LoggerFactory;
3938

40-
import org.apache.tika.exception.TikaConfigException;
41-
import org.apache.tika.extractor.DocumentSelector;
39+
import org.apache.tika.config.EmbeddedLimits;
40+
import org.apache.tika.exception.TikaException;
4241
import org.apache.tika.io.TikaInputStream;
43-
import org.apache.tika.language.detect.LanguageHandler;
4442
import org.apache.tika.metadata.Metadata;
43+
import org.apache.tika.metadata.TikaCoreProperties;
4544
import org.apache.tika.parser.ParseContext;
46-
import org.apache.tika.parser.Parser;
45+
import org.apache.tika.pipes.api.ParseMode;
46+
import org.apache.tika.sax.BasicContentHandlerFactory;
47+
import org.apache.tika.sax.ContentHandlerFactory;
48+
import org.apache.tika.server.core.TikaServerParseException;
4749

4850

4951
@Path("/meta")
@@ -63,8 +65,9 @@ public MetadataResource(TikaResource tikaResource) {
6365
public Response getMetadataFromMultipart(Attachment att, @Context UriInfo info) throws Exception {
6466
ParseContext context = tikaResource.createParseContext();
6567
try (TikaInputStream tis = TikaInputStream.get(att.getObject(InputStream.class))) {
68+
tis.getPath(); // Spool to temp file for pipes-based parsing
6669
return Response
67-
.ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), info))
70+
.ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), context))
6871
.build();
6972
}
7073
}
@@ -79,25 +82,14 @@ public Response getMetadataFromMultipart(Attachment att, @Context UriInfo info)
7982
@Path("config")
8083
public Response getMetadataWithConfig(
8184
List<Attachment> attachments,
82-
@Context HttpHeaders httpHeaders,
83-
@Context UriInfo info) throws Exception {
85+
@Context HttpHeaders httpHeaders) throws Exception {
8486

8587
// Load default context from config, then overlay with request config
8688
ParseContext context = tikaResource.createParseContext();
8789
Metadata metadata = Metadata.newInstance(context);
8890
try (TikaInputStream tis = tikaResource.setupMultipartConfig(attachments, metadata, context)) {
89-
// No need to parse embedded docs for metadata-only extraction
90-
context.set(DocumentSelector.class, metadata1 -> false);
91-
92-
Parser parser = tikaResource.createParser();
9391
TikaResource.logRequest(LOG, "/meta/config", metadata);
94-
tikaResource.parse(parser, LOG, info.getPath(), tis, new LanguageHandler() {
95-
public void endDocument() {
96-
metadata.set("language", getLanguage().getLanguage());
97-
}
98-
}, metadata, context);
99-
100-
return Response.ok(metadata).build();
92+
return Response.ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), context)).build();
10193
}
10294
}
10395

@@ -107,19 +99,19 @@ public Response getMetadata(InputStream is, @Context HttpHeaders httpHeaders, @C
10799
ParseContext context = tikaResource.createParseContext();
108100
Metadata metadata = Metadata.newInstance(context);
109101
try (TikaInputStream tis = TikaInputStream.get(is)) {
102+
tis.getPath(); // Spool to temp file for pipes-based parsing
110103
return Response
111-
.ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), info))
104+
.ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), context))
112105
.build();
113106
}
114107
}
115108

116109
/**
117-
* Get a specific metadata field. If the input stream cannot be parsed, but a
118-
* value was found for the given metadata field, then the value of the field
119-
* is returned as part of a 200 OK response; otherwise a
120-
* {@link javax.ws.rs.core.Response.Status#BAD_REQUEST} is generated. If the stream
121-
* was successfully parsed but the specific metadata field was not found, then a
122-
* {@link javax.ws.rs.core.Response.Status#NOT_FOUND} is returned.
110+
* Get a specific metadata field. If the document parses successfully but the
111+
* specific metadata field was not found, a
112+
* {@link javax.ws.rs.core.Response.Status#NOT_FOUND} is returned. Unlike the other
113+
* /meta endpoints, a bare field value has no envelope to embed a container-level
114+
* exception in, so that case is thrown (422) instead.
123115
* <p/>
124116
* Note that this method handles multivalue fields and returns possibly more
125117
* metadata value than requested.
@@ -131,35 +123,29 @@ public Response getMetadata(InputStream is, @Context HttpHeaders httpHeaders, @C
131123
* @param httpHeaders httpheaders
132124
* @param info info
133125
* @param field the tika metadata field name
134-
* @return one of {@link javax.ws.rs.core.Response.Status#OK},
135-
* {@link javax.ws.rs.core.Response.Status#NOT_FOUND}, or
136-
* {@link javax.ws.rs.core.Response.Status#BAD_REQUEST}
126+
* @return one of {@link javax.ws.rs.core.Response.Status#OK} or
127+
* {@link javax.ws.rs.core.Response.Status#NOT_FOUND}
137128
* @throws Exception
138129
*/
139130
@PUT
140131
@Path("{field}")
141132
@Produces({"text/csv", "application/json", "text/plain"})
142133
public Response getMetadataField(InputStream is, @Context HttpHeaders httpHeaders, @Context UriInfo info, @PathParam("field") String field) throws Exception {
143-
144-
// use BAD request to indicate that we may not have had enough data to
145-
// process the request
146-
Response.Status defaultErrorResponse = Response.Status.BAD_REQUEST;
147134
ParseContext context = tikaResource.createParseContext();
148-
Metadata metadata = Metadata.newInstance(context);
149-
boolean success = false;
135+
Metadata metadata;
150136
try (TikaInputStream tis = TikaInputStream.get(is)) {
151-
parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), info);
152-
// once we've parsed the document successfully, we should use NOT_FOUND
153-
// if we did not see the field
154-
defaultErrorResponse = Response.Status.NOT_FOUND;
155-
success = true;
156-
} catch (Exception e) {
157-
LOG.warn("Failed to process field {}", field, e);
137+
tis.getPath(); // Spool to temp file for pipes-based parsing
138+
metadata = parseMetadata(tis, Metadata.newInstance(context), httpHeaders.getRequestHeaders(), context);
139+
}
140+
141+
String containerException = metadata.get(TikaCoreProperties.CONTAINER_EXCEPTION);
142+
if (containerException != null && !containerException.isEmpty()) {
143+
throw new TikaServerParseException(new TikaException(containerException));
158144
}
159145

160-
if (success == false || metadata.get(field) == null) {
146+
if (metadata.get(field) == null) {
161147
return Response
162-
.status(defaultErrorResponse)
148+
.status(Response.Status.NOT_FOUND)
163149
.entity("Failed to get metadata field " + field)
164150
.build();
165151
}
@@ -175,21 +161,26 @@ public Response getMetadataField(InputStream is, @Context HttpHeaders httpHeader
175161
.build();
176162
}
177163

178-
protected Metadata parseMetadata(TikaInputStream tis, Metadata metadata, MultivaluedMap<String, String> httpHeaders, UriInfo info)
179-
throws IOException, TikaConfigException {
180-
// Load default context from config (includes DigesterFactory from parse-context)
181-
final ParseContext context = tikaResource.createParseContext();
182-
Parser parser = tikaResource.createParser();
183-
fillMetadata(parser, metadata, httpHeaders);
184-
//no need to parse embedded docs
185-
context.set(DocumentSelector.class, metadata1 -> false);
164+
/**
165+
* Parses via the shared pipes-backed PipesParser, stopping at the container document
166+
* (EmbeddedLimits maxDepth=0) with content capture off ("ignore" handler) -- metadata
167+
* only, matching /meta's contract. Set unconditionally so per-request config can't
168+
* turn content capture back on. A container-level exception is embedded in
169+
* CONTAINER_EXCEPTION here, not thrown; getMetadataField throws instead since it
170+
* returns a bare scalar with nowhere to embed it.
171+
*/
172+
protected Metadata parseMetadata(TikaInputStream tis, Metadata metadata, MultivaluedMap<String, String> httpHeaders, ParseContext context)
173+
throws Exception {
174+
fillMetadata(null, metadata, httpHeaders);
175+
context.set(EmbeddedLimits.class, new EmbeddedLimits(0, false, EmbeddedLimits.UNLIMITED, false));
176+
context.set(ContentHandlerFactory.class,
177+
new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.IGNORE, -1));
186178

187179
TikaResource.logRequest(LOG, "/meta", metadata);
188-
tikaResource.parse(parser, LOG, info.getPath(), tis, new LanguageHandler() {
189-
public void endDocument() {
190-
metadata.set("language", getLanguage().getLanguage());
191-
}
192-
}, metadata, context);
193-
return metadata;
180+
List<Metadata> metadataList = tikaResource.parseWithPipes(tis, metadata, context, ParseMode.RMETA);
181+
if (metadataList.isEmpty()) {
182+
return Metadata.newInstance(context);
183+
}
184+
return metadataList.get(0);
194185
}
195186
}

tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/StackTraceTest.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ protected Path getUnpackEmitterBasePath() {
113113
@Test
114114
public void testEncrypted() throws Exception {
115115
for (String path : PATHS) {
116-
if ("/rmeta".equals(path)) {
116+
// /rmeta and /meta embed a container exception at 200 instead of throwing 422.
117+
if ("/rmeta".equals(path) || "/meta".equals(path)) {
117118
continue;
118119
}
119120
// Use path-based routing for /tika
@@ -132,7 +133,8 @@ public void testEncrypted() throws Exception {
132133
@Test
133134
public void testNullPointerOnTika() throws Exception {
134135
for (String path : PATHS) {
135-
if ("/rmeta".equals(path)) {
136+
// Same as testEncrypted.
137+
if ("/rmeta".equals(path) || "/meta".equals(path)) {
136138
continue;
137139
}
138140
// Use path-based routing for /tika
@@ -170,10 +172,7 @@ public void testEmptyParser() throws Exception {
170172
}
171173

172174

173-
//For now, make sure that non-complete document
174-
//still returns BAD_REQUEST. We may want to
175-
//make MetadataResource return the same types of parse
176-
//exceptions as the others...
175+
// A truncated document isn't a process failure -- NOT_FOUND, not BAD_REQUEST.
177176
@Test
178177
public void testMeta() throws Exception {
179178
InputStream stream = ClassLoader.getSystemResourceAsStream(TEST_HELLO_WORLD);
@@ -183,7 +182,7 @@ public void testMeta() throws Exception {
183182
.type("application/mock+xml")
184183
.accept(MediaType.TEXT_PLAIN)
185184
.put(copy(stream, 100));
186-
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
185+
assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
187186
String msg = getStringFromInputStream((InputStream) response.getEntity());
188187
assertEquals("Failed to get metadata field Author", msg);
189188
}

tika-server/tika-server-standard/src/main/java/org/apache/tika/server/standard/resource/XMPMetadataResource.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ public Response getMetadataField(InputStream is, @Context HttpHeaders httpHeader
5858
public Response getMetadataFromMultipart(Attachment att, @Context UriInfo info) throws Exception {
5959
ParseContext context = new ParseContext();
6060
try (TikaInputStream tis = TikaInputStream.get(att.getObject(InputStream.class))) {
61+
tis.getPath(); // Spool to temp file for pipes-based parsing
6162
return Response
62-
.ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), info))
63+
.ok(parseMetadata(tis, Metadata.newInstance(context), att.getHeaders(), context))
6364
.build();
6465
}
6566
}
@@ -70,8 +71,9 @@ public Response getMetadata(InputStream is, @Context HttpHeaders httpHeaders, @C
7071
ParseContext context = new ParseContext();
7172
Metadata metadata = Metadata.newInstance(context);
7273
try (TikaInputStream tis = TikaInputStream.get(is)) {
74+
tis.getPath(); // Spool to temp file for pipes-based parsing
7375
return Response
74-
.ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), info))
76+
.ok(parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(), context))
7577
.build();
7678
}
7779
}

tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/MetadataResourceTest.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.apache.tika.metadata.TikaCoreProperties;
4848
import org.apache.tika.serialization.JsonMetadata;
4949
import org.apache.tika.server.core.CXFTestBase;
50+
import org.apache.tika.server.core.TikaServerParseExceptionMapper;
5051
import org.apache.tika.server.core.resource.MetadataResource;
5152
import org.apache.tika.server.core.writer.CSVMessageBodyWriter;
5253
import org.apache.tika.server.core.writer.JSONMessageBodyWriter;
@@ -73,6 +74,8 @@ protected void setUpResources(JAXRSServerFactoryBean sf) {
7374
@Override
7475
protected void setUpProviders(JAXRSServerFactoryBean sf) {
7576
List<Object> providers = new ArrayList<>();
77+
// Needed by getMetadataField's TikaServerParseException throw.
78+
providers.add(new TikaServerParseExceptionMapper(false));
7679
providers.add(new JSONMessageBodyWriter());
7780
providers.add(new CSVMessageBodyWriter());
7881
providers.add(new XMPMessageBodyWriter());
@@ -118,10 +121,13 @@ public void testPasswordProtected() throws Exception {
118121
.accept("application/json")
119122
.post(new MultipartBody(Arrays.asList(fileAtt)));
120123

121-
// Won't work, no password given - EncryptedDocumentException returns 422
122-
assertEquals(500, response.getStatus());
124+
// A failed decrypt isn't a process failure -- 200, exception on the metadata.
125+
assertEquals(200, response.getStatus());
126+
Metadata noPasswordMetadata = JsonMetadata.fromJson(new InputStreamReader((InputStream) response.getEntity(), UTF_8));
127+
assertContains("org.apache.tika.exception.EncryptedDocumentException",
128+
noPasswordMetadata.get(TikaCoreProperties.CONTAINER_EXCEPTION));
123129

124-
// Test 2: Wrong password - should fail
130+
// Test 2: Wrong password - should fail the same way
125131
fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.xls\"");
126132
fileAtt = new Attachment("file",
127133
ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_PASSWORD_PROTECTED), fileCd);
@@ -142,7 +148,10 @@ public void testPasswordProtected() throws Exception {
142148
.accept("application/json")
143149
.post(new MultipartBody(Arrays.asList(fileAtt, wrongConfigAtt)));
144150

145-
assertEquals(500, response.getStatus());
151+
assertEquals(200, response.getStatus());
152+
Metadata wrongPasswordMetadata = JsonMetadata.fromJson(new InputStreamReader((InputStream) response.getEntity(), UTF_8));
153+
assertContains("org.apache.tika.exception.EncryptedDocumentException",
154+
wrongPasswordMetadata.get(TikaCoreProperties.CONTAINER_EXCEPTION));
146155

147156
// Test 3: Correct password - should work
148157
fileCd = new ContentDisposition("form-data; name=\"file\"; filename=\"test.xls\"");
@@ -213,16 +222,17 @@ public void testGetField_XXX_NotFound() throws Exception {
213222
}
214223

215224
@Test
216-
public void testGetField_Author_TEXT_Partial_BAD_REQUEST() throws Exception {
217-
225+
public void testGetField_Author_TEXT_Partial_UNPROCESSABLE() throws Exception {
226+
// Truncating at 8000 bytes corrupts the OLE2 structure enough that OfficeParser
227+
// throws -- a real container exception, not just a missing field.
218228
InputStream stream = ClassLoader.getSystemResourceAsStream(TikaResourceTest.TEST_DOC);
219229

220230
Response response = WebClient
221231
.create(endPoint + META_PATH + "/Author")
222232
.type("application/msword")
223233
.accept(MediaType.TEXT_PLAIN)
224234
.put(copy(stream, 8000));
225-
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
235+
assertEquals(422, response.getStatus());
226236
}
227237

228238
@Test

0 commit comments

Comments
 (0)