This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch TIKA-4809-stage-9
in repository https://gitbox.apache.org/repos/asf/tika.git

commit e8bbbd2c4599c839041e56949125c7eaeb5c885e
Author: tallison <[email protected]>
AuthorDate: Mon Aug 10 15:56:30 2026 -0400

    TIKA-4809: Restore rdf+xml output on /meta, and gate SPI resource loading
---
 .../apache/tika/server/core/TikaServerProcess.java |  9 ++-
 .../server/core/resource/MetadataResource.java     | 10 ++-
 .../server/core/resource/TikaResourceAware.java    | 26 ++++++
 .../standard/resource/XMPMetadataResource.java     | 19 ++++-
 .../resource/ResourceServiceLoadingTest.java       | 93 ++++++++++++++++++++++
 5 files changed, 152 insertions(+), 5 deletions(-)

diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index eb5f5fe58b..8db6425753 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -82,6 +82,7 @@ import org.apache.tika.server.core.resource.TikaDetectors;
 import org.apache.tika.server.core.resource.TikaMimeTypes;
 import org.apache.tika.server.core.resource.TikaParsers;
 import org.apache.tika.server.core.resource.TikaResource;
+import org.apache.tika.server.core.resource.TikaResourceAware;
 import org.apache.tika.server.core.resource.TikaServerResource;
 import org.apache.tika.server.core.resource.TikaServerStatus;
 import org.apache.tika.server.core.resource.TikaVersion;
@@ -433,11 +434,12 @@ public class TikaServerProcess {
             resourceProviders.add(new SingletonResourceProvider(
                     new PipesResource(helper.getPipesParser(), 
helper.getPipesConfig())));
         }
-        resourceProviders.addAll(loadResourceServices(serverStatus));
+        resourceProviders.addAll(loadResourceServices(serverStatus, 
tikaResource));
         return resourceProviders;
     }
 
-    private static Collection<? extends ResourceProvider> 
loadResourceServices(ServerStatus serverStatus) {
+    private static Collection<? extends ResourceProvider> 
loadResourceServices(ServerStatus serverStatus,
+                                                                              
TikaResource tikaResource) {
         List<TikaServerResource> resources = new 
ServiceLoader(TikaServerProcess.class.getClassLoader()).loadServiceProviders(TikaServerResource.class);
         List<ResourceProvider> providers = new ArrayList<>();
         for (TikaServerResource r : resources) {
@@ -445,6 +447,9 @@ public class TikaServerProcess {
             if (r instanceof ServerStatusResource) {
                 ((ServerStatusResource) r).setServerStatus(serverStatus);
             }
+            if (r instanceof TikaResourceAware) {
+                ((TikaResourceAware) r).setTikaResource(tikaResource);
+            }
             providers.add(new SingletonResourceProvider(r));
         }
         return providers;
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 1fbc0bffdb..d9e740d324 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
@@ -52,12 +52,20 @@ import org.apache.tika.server.core.TikaServerParseException;
 public class MetadataResource {
     private static final Logger LOG = 
LoggerFactory.getLogger(MetadataResource.class);
 
-    private final TikaResource tikaResource;
+    private TikaResource tikaResource;
 
     public MetadataResource(TikaResource tikaResource) {
         this.tikaResource = tikaResource;
     }
 
+    /** For subclasses the service loader constructs; see {@link 
TikaResourceAware}. */
+    protected MetadataResource() {
+    }
+
+    protected void setTikaResource(TikaResource tikaResource) {
+        this.tikaResource = tikaResource;
+    }
+
     @POST
     @Consumes("multipart/form-data")
     @Produces({"text/csv", "application/json"})
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResourceAware.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResourceAware.java
new file mode 100644
index 0000000000..409d5acc24
--- /dev/null
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResourceAware.java
@@ -0,0 +1,26 @@
+/*
+ * 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.resource;
+
+/**
+ * For SPI-loaded resources that need the shared {@link TikaResource}. The 
service loader can
+ * only call a no-arg (or ServiceLoader-arg) constructor, so the dependency is 
injected after
+ * construction, the same way {@code ServerStatusResource} receives its 
ServerStatus.
+ */
+public interface TikaResourceAware extends TikaServerResource {
+    void setTikaResource(TikaResource tikaResource);
+}
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 6bf07562ef..d8360d3d4d 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
@@ -35,14 +35,29 @@ import org.apache.tika.metadata.Metadata;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.server.core.resource.MetadataResource;
 import org.apache.tika.server.core.resource.TikaResource;
-import org.apache.tika.server.core.resource.TikaServerResource;
+import org.apache.tika.server.core.resource.TikaResourceAware;
 
-public class XMPMetadataResource extends MetadataResource implements 
TikaServerResource {
+/**
+ * XMP is content negotiation on {@code /meta}, not a path of its own: this 
class inherits
+ * {@code @Path("/meta")} from {@link MetadataResource} and differs only in 
producing
+ * {@code application/rdf+xml}. CXF picks between the two by the request's 
Accept header.
+ */
+public class XMPMetadataResource extends MetadataResource implements 
TikaResourceAware {
 
     public XMPMetadataResource(TikaResource tikaResource) {
         super(tikaResource);
     }
 
+    /** Required by the service loader, which cannot supply the TikaResource. 
*/
+    public XMPMetadataResource() {
+        super();
+    }
+
+    @Override
+    public void setTikaResource(TikaResource tikaResource) {
+        super.setTikaResource(tikaResource);
+    }
+
     @PUT
     @Path("{field}")
     @Produces({"application/rdf+xml"})
diff --git 
a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/resource/ResourceServiceLoadingTest.java
 
b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/resource/ResourceServiceLoadingTest.java
new file mode 100644
index 0000000000..231facf191
--- /dev/null
+++ 
b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/resource/ResourceServiceLoadingTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.resource;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.config.ServiceLoader;
+import org.apache.tika.server.core.resource.TikaServerResource;
+import org.apache.tika.utils.ServiceLoaderUtils;
+
+/**
+ * Every resource this module contributes has to survive the service loader, 
which can only call
+ * a no-arg (or ServiceLoader-arg) constructor. Adding a constructor argument 
to one of them
+ * silently drops its endpoints at startup -- the failure is a WARN in the log 
and a 406 to the
+ * caller, with nothing in the test suite to notice, because the resource 
tests construct their
+ * resources directly and register them by hand.
+ * <p>
+ * That is how {@link XMPMetadataResource} lost {@code Accept: 
application/rdf+xml} on
+ * {@code /meta} after gaining a {@code (TikaResource)} constructor.
+ */
+public class ResourceServiceLoadingTest {
+
+    private static final String SPI_RESOURCE =
+            "META-INF/services/" + TikaServerResource.class.getName();
+
+    @Test
+    public void testDeclaredResourcesAreInstantiable() throws Exception {
+        List<String> declared = readDeclaredResources();
+        assertFalse(declared.isEmpty(), "expected this module to declare 
resources at " + SPI_RESOURCE);
+
+        ServiceLoader loader = new ServiceLoader(getClass().getClassLoader());
+        for (String className : declared) {
+            Class<?> klass = Class.forName(className);
+            // Throws if no usable constructor exists -- exactly what the 
server swallows.
+            Object instance = ServiceLoaderUtils.newInstance(klass, loader);
+            assertTrue(instance instanceof TikaServerResource, className + " 
must be a TikaServerResource");
+        }
+    }
+
+    /** The XMP resource in particular, since it is the one that regressed. */
+    @Test
+    public void testXmpResourceIsDeclaredAndLoadable() throws Exception {
+        
assertTrue(readDeclaredResources().contains(XMPMetadataResource.class.getName()),
+                "XMPMetadataResource must stay declared -- it is what serves 
rdf+xml from /meta");
+        ServiceLoaderUtils.newInstance(XMPMetadataResource.class,
+                new ServiceLoader(getClass().getClassLoader()));
+    }
+
+    private List<String> readDeclaredResources() throws IOException {
+        List<String> names = new ArrayList<>();
+        try (InputStream is = 
getClass().getClassLoader().getResourceAsStream(SPI_RESOURCE)) {
+            if (is == null) {
+                return names;
+            }
+            try (BufferedReader reader =
+                         new BufferedReader(new InputStreamReader(is, 
StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    line = line.trim();
+                    if (!line.isEmpty() && !line.startsWith("#")) {
+                        names.add(line);
+                    }
+                }
+            }
+        }
+        return names;
+    }
+}

Reply via email to