ARIES-2030 implement extensions

Signed-off-by: Raymond Auge <rotty3000@apache.org>
diff --git a/integrations/rest-management/rest-management-itest/src/main/java/org/apache/aries/jax/rs/rest/management/test/ExtensionsTest.java b/integrations/rest-management/rest-management-itest/src/main/java/org/apache/aries/jax/rs/rest/management/test/ExtensionsTest.java
new file mode 100644
index 0000000..a475cdf
--- /dev/null
+++ b/integrations/rest-management/rest-management-itest/src/main/java/org/apache/aries/jax/rs/rest/management/test/ExtensionsTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.aries.jax.rs.rest.management.test;
+
+import static org.apache.aries.jax.rs.rest.management.RestManagementConstants.APPLICATION_EXTENSIONS_XML_TYPE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+
+import org.apache.aries.jax.rs.rest.management.schema.ExtensionsSchema;
+import org.junit.jupiter.api.Test;
+import org.osgi.service.rest.RestApiExtension;
+import org.osgi.test.common.dictionary.Dictionaries;
+import org.osgi.test.common.stream.MapStream;
+import org.xmlunit.assertj3.XmlAssert;
+
+import net.javacrumbs.jsonunit.assertj.JsonAssertions;
+
+public class ExtensionsTest extends TestUtil {
+
+    @Test
+    public void getExtensionsJSON() {
+        WebTarget target = createDefaultTarget().path("extensions");
+
+        Response response = target.request().get();
+
+        String result = response.readEntity(String.class);
+
+        JsonAssertions.assertThatJson(
+            result
+        ).and(
+            j -> j.isObject()
+        );
+    }
+
+    @Test
+    public void getExtensionsJSON_WithService() {
+        WebTarget target = createDefaultTarget().path("extensions");
+
+        bundleContext.registerService(
+            RestApiExtension.class, new RestApiExtension() {},
+            Dictionaries.dictionaryOf(
+                RestApiExtension.NAME, "foo",
+                RestApiExtension.URI_PATH, "/foo"));
+
+        Response response = target.request().get();
+
+        String result = response.readEntity(String.class);
+
+        JsonAssertions.assertThatJson(
+            result
+        ).and(
+            j -> j.isObject(),
+            j -> j.node("extensions").isArray(),
+            j -> j.node("extensions[0]").and(
+                j1 -> j1.isObject(),
+                j1 -> j1.node("name").isEqualTo("foo"),
+                j1 -> j1.node("path").isEqualTo("/foo")
+            )
+        );
+    }
+
+    @Test
+    public void getExtensions_DOT_JSON() {
+        WebTarget target = createDefaultTarget().path("extensions.json");
+
+        Response response = target.request().get();
+
+        String result = response.readEntity(String.class);
+
+        JsonAssertions.assertThatJson(
+            result
+        ).and(
+            j -> j.isObject()
+        );
+    }
+
+    @Test
+    public void getExtensionsXML() {
+        WebTarget target = createDefaultTarget().path("extensions");
+
+        Response response = target.request(APPLICATION_EXTENSIONS_XML_TYPE).get();
+
+        String result = response.readEntity(String.class);
+
+        XmlAssert.assertThat(
+            result
+        ).isInvalid().nodesByXPath(
+            "//extensions/*"
+        ).isEmpty();
+    }
+
+    @Test
+    public void getExtensionsXML_DOT_XML() {
+        WebTarget target = createDefaultTarget().path("extensions.xml");
+
+        Response response = target.request().get();
+
+        String result = response.readEntity(String.class);
+
+        XmlAssert.assertThat(
+            result
+        ).isInvalid().nodesByXPath(
+            "//extensions/*"
+        ).isEmpty();
+    }
+
+    @Test
+    public void getExtensionsSchemaJSON() {
+        WebTarget target = createDefaultTarget().path("extensions");
+
+        Response response = target.request().get();
+
+        ExtensionsSchema extensionsSchema = response.readEntity(ExtensionsSchema.class);
+
+        assertThat(extensionsSchema.extensions.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void getExtensionsSchemaXML() {
+        WebTarget target = createDefaultTarget().path("extensions");
+
+        Response response = target.request(APPLICATION_EXTENSIONS_XML_TYPE).get();
+
+        ExtensionsSchema extensionsSchema = response.readEntity(ExtensionsSchema.class);
+
+        assertThat(extensionsSchema.extensions.size()).isEqualTo(0);
+    }
+
+}
diff --git a/integrations/rest-management/rest-management-itest/src/main/java/org/apache/aries/jax/rs/rest/management/test/TestUtil.java b/integrations/rest-management/rest-management-itest/src/main/java/org/apache/aries/jax/rs/rest/management/test/TestUtil.java
index c2ebef6..e543a9f 100644
--- a/integrations/rest-management/rest-management-itest/src/main/java/org/apache/aries/jax/rs/rest/management/test/TestUtil.java
+++ b/integrations/rest-management/rest-management-itest/src/main/java/org/apache/aries/jax/rs/rest/management/test/TestUtil.java
@@ -56,7 +56,11 @@
     @InjectService
     public ClientBuilder clientBuilder;
 
-    @InjectService(filter = "(%s=*)", filterArguments = JAX_RS_SERVICE_ENDPOINT)
+    @InjectService(
+        filter = "(%s=*)",
+        filterArguments = JAX_RS_SERVICE_ENDPOINT,
+        timeout = 400l
+    )
     public ServiceAware<JaxrsServiceRuntime> jaxrsServiceRuntimeAware;
 
     @InjectBundleContext
diff --git a/integrations/rest-management/rest-management/README.md b/integrations/rest-management/rest-management/README.md
index 877d229..b7f4b8e 100644
--- a/integrations/rest-management/rest-management/README.md
+++ b/integrations/rest-management/rest-management/README.md
@@ -8,6 +8,24 @@
 
 Since there should only be one management API per framework the integration creates a separate application rooted at `${osgi.jaxrs.endpoint}/rms`
 
+Some of the available paths are:
+
+```
+/rms/framework/bundle/{bundleid}/header
+/rms/framework/bundle/{bundleid}
+/rms/framework/bundle/{bundleid}/startlevel
+/rms/framework/bundle/{bundleid}/state
+/rms/framework/bundles/representations
+/rms/framework/bundles
+/rms/framework
+/rms/framework/service/{serviceid}
+/rms/framework/services/representations
+/rms/framework/services
+/rms/framework/startlevel
+/rms/framework/state
+/rms/extensions
+```
+
 ### Open API
 
 To simplify developer experience there is an Open API endpoint mounted at `${osgi.jaxrs.endpoint}/rms/openapi.(json|yaml)`.
diff --git a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/RestManagementConstants.java b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/RestManagementConstants.java
index 7dfd335..5161dee 100644
--- a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/RestManagementConstants.java
+++ b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/RestManagementConstants.java
@@ -109,6 +109,18 @@
     public static final MediaType APPLICATION_BUNDLESTATE_XML_TYPE =
         new MediaType("application", "org.osgi.bundlestate+xml");
 
+    public static final String APPLICATION_EXTENSIONS_JSON =
+        "application/org.osgi.extensions+json";
+
+    public static final MediaType APPLICATION_EXTENSIONS_JSON_TYPE =
+        new MediaType("application", "org.osgi.extensions+json");
+
+    public static final String APPLICATION_EXTENSIONS_XML =
+        "application/org.osgi.extensions+xml";
+
+    public static final MediaType APPLICATION_EXTENSIONS_XML_TYPE =
+        new MediaType("application", "org.osgi.extensions+xml");
+
     public static final String APPLICATION_FRAMEWORKSTARTLEVEL_JSON =
         "application/org.osgi.frameworkstartlevel+json";
 
diff --git a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/ExtensionResource.java b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/ExtensionResource.java
new file mode 100644
index 0000000..e82f318
--- /dev/null
+++ b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/ExtensionResource.java
@@ -0,0 +1,94 @@
+/*
+ * 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.aries.jax.rs.rest.management.internal;
+
+import static org.apache.aries.jax.rs.rest.management.RestManagementConstants.APPLICATION_EXTENSIONS_JSON;
+import static org.apache.aries.jax.rs.rest.management.RestManagementConstants.APPLICATION_EXTENSIONS_XML;
+
+import java.util.Optional;
+import java.util.Set;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+
+import org.apache.aries.component.dsl.CachingServiceReference;
+import org.apache.aries.jax.rs.rest.management.schema.ExtensionsSchema;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.rest.RestApiExtension;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+public class ExtensionResource extends BaseResource {
+
+    private final Set<CachingServiceReference<RestApiExtension>> extensions;
+
+    public ExtensionResource(
+        BundleContext bundleContext,
+        Set<CachingServiceReference<RestApiExtension>> extensions) {
+
+        super(bundleContext);
+        this.extensions = extensions;
+    }
+
+    @GET
+    @Path("extensions{ext: (\\.json|\\.xml)*}")
+    @Produces({APPLICATION_EXTENSIONS_JSON , APPLICATION_EXTENSIONS_XML})
+    @Operation(
+        summary = "Retrieves a Extensions Representation ",
+        responses = {
+            @ApiResponse(
+                responseCode = "200",
+                description = "The framework bundle",
+                content = @Content(schema = @Schema(implementation = ExtensionsSchema.class))
+            ),
+            @ApiResponse(
+                responseCode = "406",
+                description = "The REST management service does not support any of the requested representations"
+            )
+        }
+    )
+    public Response extensions(
+        @Parameter(allowEmptyValue = true, schema = @Schema(allowableValues = {".json", ".xml"}))
+        @PathParam("ext") String ext) {
+
+        ResponseBuilder builder = Response.status(
+            Response.Status.OK
+        ).entity(
+            ExtensionsSchema.build(extensions)
+        );
+
+        return Optional.ofNullable(
+            ext
+        ).map(
+            String::trim
+        ).map(
+            t -> ".json".equals(t) ? APPLICATION_EXTENSIONS_JSON : APPLICATION_EXTENSIONS_XML
+        ).map(t -> builder.type(t)).orElse(
+            builder
+        ).build();
+    }
+
+}
diff --git a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementActivator.java b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementActivator.java
index e742c83..4c6f143 100644
--- a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementActivator.java
+++ b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementActivator.java
@@ -19,6 +19,7 @@
 
 import static org.apache.aries.component.dsl.OSGi.all;
 import static org.apache.aries.component.dsl.OSGi.ignore;
+import static org.apache.aries.component.dsl.OSGi.just;
 import static org.apache.aries.component.dsl.OSGi.register;
 import static org.apache.aries.component.dsl.OSGi.service;
 import static org.apache.aries.component.dsl.OSGi.serviceReferences;
@@ -29,10 +30,12 @@
 
 import java.util.HashMap;
 import java.util.function.BiFunction;
+import java.util.function.Consumer;
 
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.core.Application;
 
+import org.apache.aries.component.dsl.OSGi;
 import org.apache.aries.component.dsl.OSGiResult;
 import org.apache.aries.jax.rs.rest.management.feature.RestManagementFeature;
 import org.apache.aries.jax.rs.rest.management.internal.client.RestClientFactoryImpl;
@@ -44,6 +47,7 @@
 import org.osgi.framework.PrototypeServiceFactory;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
+import org.osgi.service.rest.RestApiExtension;
 import org.osgi.service.rest.client.RestClientFactory;
 
 import io.swagger.v3.oas.models.ExternalDocumentation;
@@ -63,18 +67,31 @@
     public void start(BundleContext bundleContext) throws Exception {
         result = all(
             ignore(
-                register(
-                    Application.class,
-                    () -> new RestManagementApplication(bundleContext),
-                    () -> {
-                        HashMap<String, Object> map = new HashMap<>();
+                just(
+                    new RestManagementApplication(bundleContext)
+                ).flatMap(application ->
+                    register(
+                        Application.class,
+                        () -> application,
+                        () -> {
+                            HashMap<String, Object> map = new HashMap<>();
 
-                        map.put(JAX_RS_NAME, RestManagementApplication.class.getSimpleName());
-                        map.put(
-                            JaxrsWhiteboardConstants.JAX_RS_APPLICATION_BASE, RMS_BASE);
+                            map.put(JAX_RS_NAME, RestManagementApplication.class.getSimpleName());
+                            map.put(
+                                JaxrsWhiteboardConstants.JAX_RS_APPLICATION_BASE, RMS_BASE);
 
-                        return map;
-                    }
+                            return map;
+                        }
+                    ).then(
+                        dynamic(
+                            serviceReferences(
+                                RestApiExtension.class,
+                                "(&(org.osgi.rest.name=*)(org.osgi.rest.uri.path=*))"
+                            ),
+                            application::addExtension,
+                            application::removeExtension
+                        )
+                    )
                 )
             ),
             ignore(
@@ -164,6 +181,12 @@
         result.close();
     }
 
+    public static <T> OSGi<Void> dynamic(
+        OSGi<T> program, Consumer<T> bind, Consumer<T> unbind) {
+
+        return program.foreach(bind, unbind);
+    }
+
     class PrototypeWrapper<S> implements PrototypeServiceFactory<S> {
 
         private final BiFunction<Bundle, ServiceRegistration<S>, S> function;
diff --git a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementApplication.java b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementApplication.java
index d7d5ab3..5bec98c 100644
--- a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementApplication.java
+++ b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/internal/RestManagementApplication.java
@@ -17,17 +17,23 @@
 
 package org.apache.aries.jax.rs.rest.management.internal;
 
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
 
 import javax.ws.rs.core.Application;
 
+import org.apache.aries.component.dsl.CachingServiceReference;
 import org.apache.aries.jax.rs.rest.management.internal.jaxb.ServiceSchemaContextResolver;
 import org.osgi.framework.BundleContext;
+import org.osgi.service.rest.RestApiExtension;
 
 public class RestManagementApplication extends Application {
 
     private final BundleContext bundleContext;
+    private final Set<CachingServiceReference<RestApiExtension>> extensions =
+        new ConcurrentSkipListSet<>(Comparator.naturalOrder());
 
     public RestManagementApplication(BundleContext bundleContext) {
         this.bundleContext = bundleContext;
@@ -39,6 +45,7 @@
 
         singletons.add(new ServiceSchemaContextResolver());
 
+        singletons.add(new ExtensionResource(bundleContext, extensions));
         singletons.add(new FrameworkBundleHeaderResource(bundleContext));
         singletons.add(new FrameworkBundleResource(bundleContext));
         singletons.add(new FrameworkBundlesRepresentationsResource(bundleContext));
@@ -54,4 +61,12 @@
         return singletons;
     }
 
+    public void addExtension(CachingServiceReference<RestApiExtension> extension) {
+        extensions.add(extension);
+    }
+
+    public void removeExtension(CachingServiceReference<RestApiExtension> extension) {
+        extensions.remove(extension);
+    }
+
 }
diff --git a/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/schema/ExtensionsSchema.java b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/schema/ExtensionsSchema.java
new file mode 100644
index 0000000..a2129ce
--- /dev/null
+++ b/integrations/rest-management/rest-management/src/main/java/org/apache/aries/jax/rs/rest/management/schema/ExtensionsSchema.java
@@ -0,0 +1,67 @@
+/*
+ * 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.aries.jax.rs.rest.management.schema;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.apache.aries.component.dsl.CachingServiceReference;
+import org.osgi.service.rest.RestApiExtension;
+
+@XmlRootElement(name = "extensions")
+public class ExtensionsSchema {
+
+    @XmlElement(name = "extension")
+    public List<Extension> extensions = new ArrayList<>();
+
+    public static class Extension {
+        public String name;
+        public String path;
+        public long service;
+    }
+
+    public static ExtensionsSchema build(
+        Set<CachingServiceReference<RestApiExtension>> extensions) {
+
+        ExtensionsSchema extensionsSchema = new ExtensionsSchema();
+
+        extensions.stream().map(
+            ext -> {
+                Extension extension = new Extension();
+                extension.name = (String)ext.getProperty("org.osgi.rest.name");
+                extension.path = (String)ext.getProperty("org.osgi.rest.uri.path");
+                Optional.ofNullable(
+                    ext.getProperty("org.osgi.rest.service")
+                ).map(
+                    Long.class::cast
+                ).ifPresent(
+                    service -> extension.service = service
+                );
+                return extension;
+            }
+        ).forEach(extensionsSchema.extensions::add);
+
+        return extensionsSchema;
+    }
+
+}