[ARIES-2032] ModelConverters global registry

ModelConverters per OpenAPI instance it work because Swagger
registers the ModelConverters globally, not per OpenAPI context, so
they are used internally regardless the context that registered
them. Therefore we are just adding every registered model converter to
the global registry.
diff --git a/integrations/openapi/openapi-itest/src/main/java/test/OpenApiTest.java b/integrations/openapi/openapi-itest/src/main/java/test/OpenApiTest.java
index 3c4f4d3..77dab3e 100644
--- a/integrations/openapi/openapi-itest/src/main/java/test/OpenApiTest.java
+++ b/integrations/openapi/openapi-itest/src/main/java/test/OpenApiTest.java
@@ -20,9 +20,14 @@
 import static org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants.JAX_RS_APPLICATION_BASE;
 import static org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants.JAX_RS_APPLICATION_SELECT;
 
+import io.swagger.v3.core.converter.ModelConverter;
+import io.swagger.v3.core.jackson.ModelResolver;
+import io.swagger.v3.core.jackson.TypeNameResolver;
+import io.swagger.v3.core.util.Json;
 import io.swagger.v3.oas.models.info.Contact;
 import io.swagger.v3.oas.models.info.Info;
 import io.swagger.v3.oas.models.OpenAPI;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import org.osgi.framework.ServiceRegistration;
@@ -34,6 +39,7 @@
 import javax.ws.rs.client.WebTarget;
 
 import java.util.Hashtable;
+import java.util.Set;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -66,13 +72,88 @@
             String response = webTarget.request().get(String.class);
 
             assertTrue(response.contains("operation"));
-        }
-        finally {
+        } finally {
             serviceRegistration.unregister();
         }
     }
 
     @Test
+    public void testOpenApiEndpointWithModelResolver() {
+        OpenAPI openAPI = new OpenAPI();
+
+        openAPI.info(
+            new Info()
+                .title("My Service")
+                .description("Service REST API")
+                .contact(
+                    new Contact()
+                        .email("oschweitzer@me.com"))
+        );
+
+        Hashtable<String, Object> properties = new Hashtable<>();
+
+        properties.put("name", "test");
+
+        ServiceRegistration<OpenAPI> serviceRegistration =
+            bundleContext.registerService(
+                OpenAPI.class, openAPI, properties);
+
+        try {
+            WebTarget webTarget = createDefaultTarget().
+                path("openapi.json");
+
+            registerAddon(new TestOpenApiResource());
+
+            String response = webTarget.request().get(String.class);
+
+            assertFalse(response.contains("MyOwnClassName"));
+        } catch (Exception e) {
+            serviceRegistration.unregister();
+
+            throw e;
+        }
+
+        ServiceRegistration<ModelConverter> serviceRegistration2 =
+            bundleContext.registerService(
+                ModelConverter.class, new ModelResolver(Json.mapper(), new TypeNameResolver() {
+                    @Override
+                    protected String nameForClass(Class<?> cls, Set<Options> options) {
+                        return "MyOwnClassName";
+                    }
+                }), new Hashtable<>());
+
+        try {
+            WebTarget webTarget = createDefaultTarget().
+                path("openapi.json");
+
+            registerAddon(new TestOpenApiResource());
+
+            String response = webTarget.request().get(String.class);
+
+            assertTrue(response.contains("MyOwnClassName"));
+        } catch (Exception e) {
+            serviceRegistration.unregister();
+        } finally {
+            serviceRegistration2.unregister();
+        }
+
+        try {
+            WebTarget webTarget = createDefaultTarget().
+                path("openapi.json");
+
+            registerAddon(new TestOpenApiResource());
+
+            String response = webTarget.request().get(String.class);
+
+            assertFalse(response.contains("MyOwnClassName"));
+        } catch (Exception e) {
+            serviceRegistration.unregister();
+
+            throw e;
+        }
+    }
+
+    @Test
     public void testIncludeStaticResource() {
         String applicationSelectFilter = "(" + JAX_RS_APPLICATION_BASE +
             "=/test-application-a)";
@@ -92,7 +173,7 @@
                         .email("oschweitzer@me.com"))
         );
 
-        @SuppressWarnings({ "unchecked", "rawtypes", "serial" })
+        @SuppressWarnings({"unchecked", "rawtypes", "serial"})
         ServiceRegistration<OpenAPI> serviceRegistration =
             bundleContext.registerService(
                 OpenAPI.class, openAPI, new Hashtable() {{
@@ -123,8 +204,7 @@
             response = webTarget.request().get(String.class);
 
             assertFalse(response.contains("\"/operation\":"));
-        }
-        finally {
+        } finally {
             serviceRegistration.unregister();
         }
     }
diff --git a/integrations/openapi/openapi-itest/src/main/java/test/types/TestOpenApiResource.java b/integrations/openapi/openapi-itest/src/main/java/test/types/TestOpenApiResource.java
index 6c23e86..9d0550d 100644
--- a/integrations/openapi/openapi-itest/src/main/java/test/types/TestOpenApiResource.java
+++ b/integrations/openapi/openapi-itest/src/main/java/test/types/TestOpenApiResource.java
@@ -30,8 +30,11 @@
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/operation")
     @Operation(description = "operation")
-    public String getJsonObject() {
-        return "operation";
+    public MyReturnType getJsonObject() {
+        return null;
     }
 
+    public static class MyReturnType {
+
+    }
 }
diff --git a/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenAPIWithModelResolvers.java b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenAPIWithModelResolvers.java
new file mode 100644
index 0000000..63f5b89
--- /dev/null
+++ b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenAPIWithModelResolvers.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
+ * <p>
+ * The contents of this file are subject to the terms of the Liferay Enterprise
+ * Subscription License ("License"). You may not use this file except in
+ * compliance with the License. You can obtain a copy of the License by
+ * contacting Liferay, Inc. See the License for the specific language governing
+ * permissions and limitations under the License, including but not limited to
+ * distribution rights of the Software.
+ */
+
+package org.apache.aries.jax.rs.openapi;
+
+import io.swagger.v3.core.converter.ModelConverter;
+import io.swagger.v3.core.jackson.ModelResolver;
+import io.swagger.v3.oas.models.OpenAPI;
+import org.apache.aries.component.dsl.CachingServiceReference;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Carlos Sierra Andrés
+ */
+class OpenAPIWithModelResolvers {
+    private final CachingServiceReference<OpenAPI> openAPI;
+    private final Set<CachingServiceReference<ModelConverter>> modelConverters;
+
+    OpenAPIWithModelResolvers(
+        CachingServiceReference<OpenAPI> openAPI,
+        Set<CachingServiceReference<ModelConverter>> modelConverters) {
+
+        this.openAPI = openAPI;
+        this.modelConverters = modelConverters;
+    }
+
+    public Set<CachingServiceReference<ModelConverter>> getModelConverters() {
+        return modelConverters;
+    }
+
+    public CachingServiceReference<OpenAPI> getOpenAPIServiceReference() {
+        return openAPI;
+    }
+}
diff --git a/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiBundleActivator.java b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiBundleActivator.java
index 56f1866..df46287 100644
--- a/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiBundleActivator.java
+++ b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiBundleActivator.java
@@ -17,6 +17,8 @@
 
 package org.apache.aries.jax.rs.openapi;
 
+import io.swagger.v3.core.converter.ModelConverter;
+import io.swagger.v3.core.converter.ModelConverters;
 import io.swagger.v3.oas.models.OpenAPI;
 import org.apache.aries.component.dsl.CachingServiceReference;
 import org.apache.aries.component.dsl.OSGi;
@@ -25,8 +27,10 @@
 import org.osgi.framework.*;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
 import static org.apache.aries.component.dsl.OSGi.*;
+import static org.apache.aries.component.dsl.Utils.accumulate;
 
 /**
  * @author Carlos Sierra Andrés
@@ -36,24 +40,115 @@
 
     private OSGiResult result;
 
+    private static <T> OSGi<Set<T>> sequence(Set<OSGi<T>> list) {
+        Set<T> objects = new HashSet<>();
+
+        OSGi<Set<T>> result = just(() -> objects);
+
+        for (OSGi<T> osgi : list) {
+            result = osgi.effects(objects::add, objects::remove).then(result);
+        }
+
+        return result;
+    }
+
+    private static OpenAPIWithModelResolvers pairModelConverterWithOpenAPI(
+        CachingServiceReference<OpenAPI> oasr, List<CachingServiceReference<ModelConverter>> mcsrs) {
+
+        return new OpenAPIWithModelResolvers(
+            oasr,
+            mcsrs.stream().filter(
+                mcsr -> filterModelConverter(oasr, mcsr)
+            ).collect(
+                Collectors.toSet()
+            )
+        );
+    }
+
+    private static boolean filterModelConverter(
+        CachingServiceReference<OpenAPI> oasr,
+        CachingServiceReference<ModelConverter> mcsr) {
+
+        final String[] propertyValues = canonicalize(
+            mcsr.getProperty("osgi.jaxrs.openapi.select"));
+
+        for (String propertyValue : propertyValues) {
+            if (propertyValue == null || propertyValue.isEmpty()) {
+                continue;
+            }
+            try {
+                Filter filter = FrameworkUtil.createFilter(propertyValue);
+
+                if (filter.match(oasr.getServiceReference())) {
+                    return true;
+                }
+            } catch (InvalidSyntaxException ise) {
+                //log
+                continue;
+            }
+        }
+
+        return false;
+    }
+
+    private static String[] canonicalize(Object propertyValue) {
+        if (propertyValue == null) {
+            return new String[0];
+        }
+        if (propertyValue instanceof String[]) {
+            return (String[]) propertyValue;
+        }
+        if (propertyValue instanceof Collection) {
+            return
+                ((Collection<?>) propertyValue).stream().
+                    map(
+                        Object::toString
+                    ).toArray(
+                    String[]::new
+                );
+        }
+
+        return new String[]{propertyValue.toString()};
+    }
+
     @Override
     public void start(BundleContext bundleContext) throws Exception {
-        OSGi<?> program =
-            serviceReferences(OpenAPI.class).flatMap(sr ->
-            service(sr).flatMap(openAPI ->
-            just(
-                new OpenApiPrototypeServiceFactory(
-                    new PropertyWrapper(sr),
-                    openAPI))).flatMap(factory ->
-            register(
-                Object.class,
-                factory,
-                () -> getProperties(sr))
-            ));
+        final OSGi<?> modelConverters = services(
+            ModelConverter.class
+        ).effects(
+            ModelConverters.getInstance()::addConverter,
+            ModelConverters.getInstance()::removeConverter
+        );
+
+        OSGi<?> program = combine(
+            (openAPI, __) -> openAPI,
+            serviceReferences(OpenAPI.class),
+            accumulate(modelConverters)
+        ).
+            flatMap(openAPICachingServiceReference ->
+                service(openAPICachingServiceReference).flatMap(openAPI ->
+                    register(
+                        Object.class,
+                        new OpenApiPrototypeServiceFactory(
+                            new PropertyWrapper(openAPICachingServiceReference), openAPI),
+                        () -> getProperties(openAPICachingServiceReference)
+                    )
+                )
+            );
 
         result = program.run(bundleContext);
     }
 
+    private Set<OSGi<ModelConverter>> lazilyGetModelConverters(
+        OpenAPIWithModelResolvers openAPIWithModelConverters) {
+
+        return openAPIWithModelConverters.
+            getModelConverters().
+            stream().
+            map(OSGi::service).
+            collect(Collectors.toSet());
+    }
+
     @Override
     public void stop(BundleContext bundleContext) throws Exception {
         result.close();
diff --git a/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiPrototypeServiceFactory.java b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiPrototypeServiceFactory.java
index 1083d26..67e70fe 100644
--- a/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiPrototypeServiceFactory.java
+++ b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiPrototypeServiceFactory.java
@@ -45,9 +45,8 @@
         Bundle bundle,
         ServiceRegistration<Object> serviceRegistration) {
 
-        SwaggerConfiguration
-                swaggerConfiguration = new SwaggerConfiguration().
-                openAPI(openAPI);
+        SwaggerConfiguration swaggerConfiguration = new SwaggerConfiguration().
+            openAPI(openAPI);
 
         propertyWrapper.applyLong("cache.ttl", swaggerConfiguration::setCacheTTL);
         propertyWrapper.applyString("id", swaggerConfiguration::id);
@@ -55,7 +54,8 @@
         propertyWrapper.applyBoolean("pretty.print", swaggerConfiguration::setPrettyPrint);
         propertyWrapper.applyBoolean("read.all.resources", swaggerConfiguration::setReadAllResources);
 
-        OpenApiResource openApiResource = new OpenApiResource();
+        OpenApiResource openApiResource = new OpenApiResource(
+            (long) serviceRegistration.getReference().getProperty("service.id"));
 
         openApiResource.setOpenApiConfiguration(swaggerConfiguration);
 
diff --git a/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiResource.java b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiResource.java
index 662cf5f..a4cc6c3 100644
--- a/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiResource.java
+++ b/integrations/openapi/openapi-resource/src/main/java/org/apache/aries/jax/rs/openapi/OpenApiResource.java
@@ -7,12 +7,8 @@
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
-import javax.ws.rs.core.Application;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.*;
+import javax.ws.rs.ext.Providers;
 
 import org.apache.aries.jax.rs.whiteboard.ApplicationClasses;
 
@@ -36,6 +32,17 @@
 
     @Context
     ServletConfig config;
+    private long serviceId;
+
+    @Context
+    Providers providers;
+
+    @Context
+    Configuration configuration;
+
+    public OpenApiResource(long serviceId) {
+        this.serviceId = serviceId;
+    }
 
     @GET
     @Produces({MediaType.APPLICATION_JSON, "application/yaml"})
@@ -45,7 +52,9 @@
                                @PathParam("type") String type) throws Exception {
 
         String ctxId = app.getClass().getCanonicalName()
-            .concat("#").concat(String.valueOf(System.identityHashCode(app)));
+            .concat("#").
+                concat(String.valueOf(System.identityHashCode(app))).
+                concat(String.valueOf(this.serviceId));
 
         OpenApiContext ctx = new JaxrsOpenApiContextBuilder<>()
             .servletConfig(config)
@@ -53,10 +62,12 @@
             .configLocation(configLocation)
             .openApiConfiguration(openApiConfiguration)
             .ctxId(ctxId)
-            .buildContext(true);
+            .buildContext(false);
 
         ctx.setOpenApiScanner(new JaxrsWhiteboardScanner(applicationClasses));
 
+        ctx.init();
+
         OpenAPI oas = ctx.read();
 
         if (oas == null) {
@@ -67,14 +78,14 @@
 
         if (Optional.ofNullable(type).map(String::trim).map("yaml"::equalsIgnoreCase).orElse(Boolean.FALSE)) {
             return Response.status(Response.Status.OK)
-                  .entity(pretty ? Yaml.pretty(oas) : Yaml.mapper().writeValueAsString(oas))
-                  .type("application/yaml")
-                  .build();
+                .entity(pretty ? Yaml.pretty(oas) : Yaml.mapper().writeValueAsString(oas))
+                .type("application/yaml")
+                .build();
         } else {
             return Response.status(Response.Status.OK)
-                  .entity(pretty ? Json.pretty(oas) : Json.mapper().writeValueAsString(oas))
-                  .type(MediaType.APPLICATION_JSON_TYPE)
-                  .build();
+                .entity(pretty ? Json.pretty(oas) : Json.mapper().writeValueAsString(oas))
+                .type(MediaType.APPLICATION_JSON_TYPE)
+                .build();
         }
     }