GERONIMO-6659 ensure yaml is set as default when relevant + GERONIMO-6660 fixing...
authorRomain Manni-Bucau <rmannibucau@gmail.com>
Fri, 30 Nov 2018 14:52:49 +0000 (15:52 +0100)
committerRomain Manni-Bucau <rmannibucau@gmail.com>
Fri, 30 Nov 2018 14:52:49 +0000 (15:52 +0100)
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/cdi/GeronimoOpenAPIExtension.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/loader/yaml/Yaml.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/jaxrs/BaseOpenAPIYamlBodyWriter.java [new file with mode: 0644]
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/jaxrs/JacksonOpenAPIYamlBodyWriter.java [new file with mode: 0644]
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/jaxrs/OpenAPIFilter.java
geronimo-openapi-impl/src/test/java/org/apache/geronimo/microprofile/openapi/test/YamlAnswerTest.java [new file with mode: 0644]

index 043be2f..23cbb3d 100644 (file)
@@ -18,6 +18,7 @@ package org.apache.geronimo.microprofile.openapi.cdi;
 
 import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.toSet;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.AnnotatedElement;
@@ -31,6 +32,7 @@ import java.util.stream.Stream;
 
 import javax.enterprise.event.Observes;
 import javax.enterprise.inject.Instance;
+import javax.enterprise.inject.spi.AfterDeploymentValidation;
 import javax.enterprise.inject.spi.Annotated;
 import javax.enterprise.inject.spi.AnnotatedMethod;
 import javax.enterprise.inject.spi.Bean;
@@ -38,17 +40,22 @@ import javax.enterprise.inject.spi.BeanManager;
 import javax.enterprise.inject.spi.BeforeBeanDiscovery;
 import javax.enterprise.inject.spi.CDI;
 import javax.enterprise.inject.spi.Extension;
+import javax.enterprise.inject.spi.ProcessAnnotatedType;
 import javax.enterprise.inject.spi.ProcessBean;
 import javax.ws.rs.Path;
 import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
 
 import org.apache.geronimo.microprofile.openapi.config.GeronimoOpenAPIConfig;
 import org.apache.geronimo.microprofile.openapi.impl.filter.FilterImpl;
 import org.apache.geronimo.microprofile.openapi.impl.loader.DefaultLoader;
+import org.apache.geronimo.microprofile.openapi.impl.loader.yaml.Yaml;
 import org.apache.geronimo.microprofile.openapi.impl.model.PathsImpl;
 import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotatedMethodElement;
 import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotatedTypeElement;
 import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotationProcessor;
+import org.apache.geronimo.microprofile.openapi.jaxrs.JacksonOpenAPIYamlBodyWriter;
+import org.apache.geronimo.microprofile.openapi.jaxrs.OpenAPIFilter;
 import org.eclipse.microprofile.openapi.OASConfig;
 import org.eclipse.microprofile.openapi.OASFilter;
 import org.eclipse.microprofile.openapi.OASModelReader;
@@ -67,6 +74,7 @@ public class GeronimoOpenAPIExtension implements Extension {
     private Collection<String> packages;
     private Collection<String> excludePackages;
     private Collection<String> excludeClasses;
+    private boolean jacksonIsPresent;
 
     void init(@Observes final BeforeBeanDiscovery beforeBeanDiscovery) {
         config = GeronimoOpenAPIConfig.create();
@@ -76,6 +84,18 @@ public class GeronimoOpenAPIExtension implements Extension {
         packages = getConfigCollection(OASConfig.SCAN_PACKAGES);
         excludePackages = getConfigCollection(OASConfig.SCAN_EXCLUDE_PACKAGES);
         excludeClasses = getConfigCollection(OASConfig.SCAN_EXCLUDE_CLASSES);
+        try {
+            Yaml.getObjectMapper();
+            jacksonIsPresent = true;
+        } catch (final Error | RuntimeException e) {
+            // no-op
+        }
+    }
+
+    void vetoJacksonIfNotHere(@Observes final ProcessAnnotatedType<JacksonOpenAPIYamlBodyWriter> event) {
+        if (!jacksonIsPresent) {
+            event.veto();
+        }
     }
 
     <T> void findEndpointsAndApplication(@Observes final ProcessBean<T> event) {
@@ -87,6 +107,14 @@ public class GeronimoOpenAPIExtension implements Extension {
         }
     }
 
+    void afterValidation(@Observes final AfterDeploymentValidation validation,
+                         final BeanManager beanManager) {
+        final OpenAPIFilter filter = OpenAPIFilter.class.cast(
+                beanManager.getReference(beanManager.resolve(beanManager.getBeans(OpenAPIFilter.class)),
+                        OpenAPIFilter.class, beanManager.createCreationalContext(null)));
+        filter.setDefaultMediaType(jacksonIsPresent ? new MediaType("text", "vnd.yaml") : APPLICATION_JSON_TYPE);
+    }
+
     public OpenAPI getOrCreateOpenAPI(final Application application) {
         if (classes != null) {
             final ClassLoader loader = Thread.currentThread().getContextClassLoader();
index d958a19..b7124ee 100644 (file)
  */
 package org.apache.geronimo.microprofile.openapi.impl.loader.yaml;
 
+import static java.util.Optional.ofNullable;
+
 import java.io.IOException;
 import java.io.InputStream;
+import java.math.BigDecimal;
 import java.util.stream.Stream;
 
 import javax.enterprise.inject.Vetoed;
+import javax.json.bind.annotation.JsonbProperty;
 import javax.json.bind.annotation.JsonbTransient;
 
 import org.apache.geronimo.microprofile.openapi.impl.model.APIResponseImpl;
@@ -92,15 +96,21 @@ import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
 import org.eclipse.microprofile.openapi.models.servers.ServerVariables;
 import org.eclipse.microprofile.openapi.models.tags.Tag;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyName;
 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.databind.introspect.Annotated;
 import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
 import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver;
 import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 
 @Vetoed
@@ -111,86 +121,146 @@ public final class Yaml {
 
     public static OpenAPI loadAPI(final InputStream stream) {
         try {
-            final SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver();
-            resolver.addMapping(APIResponse.class, APIResponseImpl.class);
-            resolver.addMapping(APIResponses.class, APIResponsesImpl.class);
-            resolver.addMapping(Callback.class, CallbackImpl.class);
-            resolver.addMapping(Components.class, ComponentsImpl.class);
-            resolver.addMapping(Contact.class, ContactImpl.class);
-            resolver.addMapping(Content.class, ContentImpl.class);
-            resolver.addMapping(Discriminator.class, DiscriminatorImpl.class);
-            resolver.addMapping(Encoding.class, EncodingImpl.class);
-            resolver.addMapping(Example.class, ExampleImpl.class);
-            resolver.addMapping(Extensible.class, ExtensibleImpl.class);
-            resolver.addMapping(ExternalDocumentation.class, ExternalDocumentationImpl.class);
-            resolver.addMapping(Header.class, HeaderImpl.class);
-            resolver.addMapping(Info.class, InfoImpl.class);
-            resolver.addMapping(License.class, LicenseImpl.class);
-            resolver.addMapping(Link.class, LinkImpl.class);
-            resolver.addMapping(MediaType.class, MediaTypeImpl.class);
-            resolver.addMapping(OAuthFlow.class, OAuthFlowImpl.class);
-            resolver.addMapping(OAuthFlows.class, OAuthFlowsImpl.class);
-            resolver.addMapping(OpenAPI.class, OpenAPIImpl.class);
-            resolver.addMapping(Operation.class, OperationImpl.class);
-            resolver.addMapping(Parameter.class, ParameterImpl.class);
-            resolver.addMapping(PathItem.class, PathItemImpl.class);
-            resolver.addMapping(Paths.class, PathsImpl.class);
-            resolver.addMapping(Reference.class, ReferenceImpl.class);
-            resolver.addMapping(RequestBody.class, RequestBodyImpl.class);
-            resolver.addMapping(Schema.class, SchemaImpl.class);
-            resolver.addMapping(Scopes.class, ScopesImpl.class);
-            resolver.addMapping(SecurityRequirement.class, SecurityRequirementImpl.class);
-            resolver.addMapping(SecurityScheme.class, SecuritySchemeImpl.class);
-            resolver.addMapping(Server.class, ServerImpl.class);
-            resolver.addMapping(ServerVariable.class, ServerVariableImpl.class);
-            resolver.addMapping(ServerVariables.class, ServerVariablesImpl.class);
-            resolver.addMapping(Tag.class, TagImpl.class);
-            resolver.addMapping(XML.class, XMLImpl.class);
-
-            final SimpleModule module = new SimpleModule();
-            module.setAbstractTypes(resolver);
-            module.addDeserializer(Parameter.In.class, new JsonDeserializer<Parameter.In>() {
-                @Override
-                public Parameter.In deserialize(final JsonParser p, final DeserializationContext ctxt) {
-                    return Stream.of(Parameter.In.values()).filter(it -> {
-                        try {
-                            return it.name().equalsIgnoreCase(p.getValueAsString());
-                        } catch (IOException e) {
-                            throw new IllegalArgumentException(e);
-                        }
-                    }).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching In value"));
-                }
-            });
-            module.addDeserializer(Schema.SchemaType.class, new JsonDeserializer<Schema.SchemaType>() {
-                @Override
-                public Schema.SchemaType deserialize(final JsonParser p, final DeserializationContext ctxt) {
-                    return Stream.of(Schema.SchemaType.values()).filter(it -> {
-                        try {
-                            return it.name().equalsIgnoreCase(p.getValueAsString());
-                        } catch (IOException e) {
-                            throw new IllegalArgumentException(e);
-                        }
-                    }).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching SchemaType value"));
-                }
-            });
-
-            final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
-            mapper.registerModule(module);
-            mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
-                @Override
-                protected boolean _isIgnorable(final Annotated a) {
-                    return super._isIgnorable(a) || a.getAnnotation(JsonbTransient.class) != null;
-                }
-            });
-            mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
-                @Override
-                public String translate(final String propertyName) {
-                    return "ref".equals(propertyName) ? "$ref" : propertyName;
-                }
-            });
+            final ObjectMapper mapper = getObjectMapper();
             return mapper.readValue(stream, OpenAPI.class);
         } catch (final IOException e) {
             throw new IllegalArgumentException(e);
         }
     }
+
+    // let be reusable in integrations
+    public static ObjectMapper getObjectMapper() {
+        final SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver();
+        resolver.addMapping(APIResponse.class, APIResponseImpl.class);
+        resolver.addMapping(APIResponses.class, APIResponsesImpl.class);
+        resolver.addMapping(Callback.class, CallbackImpl.class);
+        resolver.addMapping(Components.class, ComponentsImpl.class);
+        resolver.addMapping(Contact.class, ContactImpl.class);
+        resolver.addMapping(Content.class, ContentImpl.class);
+        resolver.addMapping(Discriminator.class, DiscriminatorImpl.class);
+        resolver.addMapping(Encoding.class, EncodingImpl.class);
+        resolver.addMapping(Example.class, ExampleImpl.class);
+        resolver.addMapping(Extensible.class, ExtensibleImpl.class);
+        resolver.addMapping(ExternalDocumentation.class, ExternalDocumentationImpl.class);
+        resolver.addMapping(Header.class, HeaderImpl.class);
+        resolver.addMapping(Info.class, InfoImpl.class);
+        resolver.addMapping(License.class, LicenseImpl.class);
+        resolver.addMapping(Link.class, LinkImpl.class);
+        resolver.addMapping(MediaType.class, MediaTypeImpl.class);
+        resolver.addMapping(OAuthFlow.class, OAuthFlowImpl.class);
+        resolver.addMapping(OAuthFlows.class, OAuthFlowsImpl.class);
+        resolver.addMapping(OpenAPI.class, OpenAPIImpl.class);
+        resolver.addMapping(Operation.class, OperationImpl.class);
+        resolver.addMapping(Parameter.class, ParameterImpl.class);
+        resolver.addMapping(PathItem.class, PathItemImpl.class);
+        resolver.addMapping(Paths.class, PathsImpl.class);
+        resolver.addMapping(Reference.class, ReferenceImpl.class);
+        resolver.addMapping(RequestBody.class, RequestBodyImpl.class);
+        resolver.addMapping(Schema.class, SchemaImpl.class);
+        resolver.addMapping(Scopes.class, ScopesImpl.class);
+        resolver.addMapping(SecurityRequirement.class, SecurityRequirementImpl.class);
+        resolver.addMapping(SecurityScheme.class, SecuritySchemeImpl.class);
+        resolver.addMapping(Server.class, ServerImpl.class);
+        resolver.addMapping(ServerVariable.class, ServerVariableImpl.class);
+        resolver.addMapping(ServerVariables.class, ServerVariablesImpl.class);
+        resolver.addMapping(Tag.class, TagImpl.class);
+        resolver.addMapping(XML.class, XMLImpl.class);
+
+        final SimpleModule module = new SimpleModule();
+        module.setAbstractTypes(resolver);
+        module.addDeserializer(Parameter.In.class, new JsonDeserializer<Parameter.In>() {
+            @Override
+            public Parameter.In deserialize(final JsonParser p, final DeserializationContext ctxt) {
+                return Stream.of(Parameter.In.values()).filter(it -> {
+                    try {
+                        return it.name().equalsIgnoreCase(p.getValueAsString());
+                    } catch (IOException e) {
+                        throw new IllegalArgumentException(e);
+                    }
+                }).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching In value"));
+            }
+        });
+        module.addDeserializer(Schema.SchemaType.class, new JsonDeserializer<Schema.SchemaType>() {
+            @Override
+            public Schema.SchemaType deserialize(final JsonParser p, final DeserializationContext ctxt) {
+                return Stream.of(Schema.SchemaType.values()).filter(it -> {
+                    try {
+                        return it.name().equalsIgnoreCase(p.getValueAsString());
+                    } catch (IOException e) {
+                        throw new IllegalArgumentException(e);
+                    }
+                }).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching SchemaType value"));
+            }
+        });
+        module.addDeserializer(SecurityScheme.Type.class, new JsonDeserializer<SecurityScheme.Type>() {
+            @Override
+            public SecurityScheme.Type deserialize(final JsonParser p, final DeserializationContext ctxt) {
+                return Stream.of(SecurityScheme.Type.values()).filter(it -> {
+                    try {
+                        return it.name().equalsIgnoreCase(p.getValueAsString());
+                    } catch (IOException e) {
+                        throw new IllegalArgumentException(e);
+                    }
+                }).findFirst().orElseThrow(() -> new IllegalArgumentException("No matching SecurityScheme.Type value"));
+            }
+        });
+        module.addSerializer(BigDecimal.class, new NumberSerializer(BigDecimal.class) {
+            @Override
+            public void serialize(final Number value, final JsonGenerator g,
+                                  final SerializerProvider provider) throws IOException {
+                if (BigDecimal.class.isInstance(value) && value.doubleValue() == value.longValue()) {
+                    super.serialize(value.longValue(), g, provider);
+                } else {
+                    super.serialize(value, g, provider);
+                }
+            }
+        });
+        Stream.of(SecurityScheme.Type.class, SecurityScheme.In.class,
+                Schema.SchemaType.class, Header.Style.class, Encoding.Style.class,
+                Parameter.Style.class, Parameter.In.class)
+              .forEach(it -> toStringSerializer(module, it));
+
+        final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+        mapper.registerModule(module);
+        mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
+            @Override
+            protected boolean _isIgnorable(final Annotated a) {
+                return super._isIgnorable(a) || a.getAnnotation(JsonbTransient.class) != null;
+            }
+
+            @Override
+            public PropertyName findNameForSerialization(final Annotated a) {
+                return ofNullable(a.getAnnotation(JsonbProperty.class))
+                        .map(JsonbProperty::value)
+                        .map(PropertyName::new)
+                        .orElseGet(() -> super.findNameForSerialization(a));
+            }
+
+            @Override
+            public PropertyName findNameForDeserialization(final Annotated a) {
+                return ofNullable(a.getAnnotation(JsonbProperty.class))
+                        .map(JsonbProperty::value)
+                        .map(PropertyName::new)
+                        .orElseGet(() -> super.findNameForDeserialization(a));
+            }
+        });
+        mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
+            @Override
+            public String translate(final String propertyName) {
+                return "ref".equals(propertyName) ? "$ref" : propertyName;
+            }
+        });
+        return mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
+    }
+
+    private static <T> void toStringSerializer(final SimpleModule module, final Class<T> it) {
+        module.addSerializer(it, new JsonSerializer<T>() {
+            @Override
+            public void serialize(final T value,
+                                  final JsonGenerator gen,
+                                  final SerializerProvider serializers) throws IOException {
+                gen.writeString(value.toString());
+            }
+        });
+    }
 }
diff --git a/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/jaxrs/BaseOpenAPIYamlBodyWriter.java b/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/jaxrs/BaseOpenAPIYamlBodyWriter.java
new file mode 100644 (file)
index 0000000..fad1d6f
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.geronimo.microprofile.openapi.jaxrs;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.MessageBodyWriter;
+
+// potential base to use snakeyaml or another serializer
+public abstract class BaseOpenAPIYamlBodyWriter<T> implements MessageBodyWriter<T> {
+    @Override
+    public boolean isWriteable(final Class<?> type, final Type genericType,
+                               final Annotation[] annotations, final MediaType mediaType) {
+        return type.getName().startsWith("org.apache.geronimo.microprofile.openapi.impl.model") ||
+               type.getName().startsWith("org.eclipse.microprofile.openapi.models");
+    }
+
+    @Override
+    public long getSize(final T t, final Class<?> type, final Type genericType,
+                        final Annotation[] annotations, final MediaType mediaType) {
+        return -1;
+    }
+}
diff --git a/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/jaxrs/JacksonOpenAPIYamlBodyWriter.java b/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/jaxrs/JacksonOpenAPIYamlBodyWriter.java
new file mode 100644 (file)
index 0000000..6bf946f
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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.geronimo.microprofile.openapi.jaxrs;
+
+import static javax.ws.rs.RuntimeType.SERVER;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.enterprise.context.Dependent;
+import javax.ws.rs.ConstrainedTo;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.geronimo.microprofile.openapi.impl.loader.yaml.Yaml;
+
+@Provider
+@Dependent
+@ConstrainedTo(SERVER)
+@Produces({
+        "text/vnd.yaml", "text/yaml", "text/x-yaml",
+        "application/vnd.yaml", "application/yaml", "application/x-yaml"})
+public class JacksonOpenAPIYamlBodyWriter<T> extends BaseOpenAPIYamlBodyWriter<T> {
+    @Override
+    public void writeTo(final T entity, final Class<?> type, final Type genericType,
+                        final Annotation[] annotations, final MediaType mediaType,
+                        final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream)
+            throws IOException, WebApplicationException {
+        Mapper.get().writeValue(entityStream, entity);
+    }
+
+    private static class Mapper {
+        private static final AtomicReference<com.fasterxml.jackson.databind.ObjectMapper> REF = new AtomicReference<>();
+
+        private Mapper() {
+            // no-op
+        }
+
+        public static com.fasterxml.jackson.databind.ObjectMapper get() {
+            // for now we only support jackson, ok while this is actually only tck code ;)
+            com.fasterxml.jackson.databind.ObjectMapper mapper = REF.get();
+            if (mapper == null) {
+                mapper = Yaml.getObjectMapper();
+                if (!REF.compareAndSet(null, mapper)) {
+                    mapper = REF.get();
+                }
+            }
+            return mapper;
+        }
+    }
+}
index c9de942..494ac65 100644 (file)
  */
 package org.apache.geronimo.microprofile.openapi.jaxrs;
 
-import static java.util.Optional.ofNullable;
 import static javax.ws.rs.Priorities.USER;
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
+import static javax.ws.rs.core.MediaType.WILDCARD_TYPE;
+
+import java.util.List;
 
 import javax.annotation.Priority;
 import javax.enterprise.context.ApplicationScoped;
@@ -29,6 +31,7 @@ import javax.ws.rs.container.ContainerRequestFilter;
 import javax.ws.rs.container.PreMatching;
 import javax.ws.rs.core.Application;
 import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.Provider;
 
@@ -47,6 +50,7 @@ public class OpenAPIFilter implements ContainerRequestFilter {
     private GeronimoOpenAPIExtension extension;
 
     private OpenAPI openApi;
+    private MediaType defaultMediaType;
 
     @Override
     public void filter(final ContainerRequestContext rc) {
@@ -55,15 +59,33 @@ public class OpenAPIFilter implements ContainerRequestFilter {
         }
         final String path = rc.getUriInfo().getPath();
         if ("openapi".equals(path)) {
-            rc.abortWith(Response.ok(openApi).type(ofNullable(rc.getMediaType()).orElse(APPLICATION_JSON_TYPE)).build());
+            final List<MediaType> mediaTypes = rc.getAcceptableMediaTypes();
+            rc.abortWith(Response.ok(openApi).type(selectType(mediaTypes)).build());
         }
         if ("openapi.json".equals(path)) {
             rc.abortWith(Response.ok(openApi).type(APPLICATION_JSON_TYPE).build());
         }
+        if ("openapi.yml".equals(path) || "openapi.yaml".equals(path)) {
+            rc.abortWith(Response.ok(openApi).type("text/vnd.yaml").build());
+        }
+    }
+
+    private MediaType selectType(final List<MediaType> mediaTypes) {
+        if (mediaTypes.contains(APPLICATION_JSON_TYPE)) {
+            return APPLICATION_JSON_TYPE;
+        }
+        if (mediaTypes.isEmpty()) {
+            return defaultMediaType;
+        }
+        return mediaTypes.stream().filter(it -> !WILDCARD_TYPE.equals(it)).findFirst().orElse(defaultMediaType);
     }
 
     @Context
     public void setApplication(final Application application) {
         this.openApi = extension.getOrCreateOpenAPI(application);
     }
+
+    public void setDefaultMediaType(final MediaType defaultMediaType) {
+        this.defaultMediaType = defaultMediaType;
+    }
 }
diff --git a/geronimo-openapi-impl/src/test/java/org/apache/geronimo/microprofile/openapi/test/YamlAnswerTest.java b/geronimo-openapi-impl/src/test/java/org/apache/geronimo/microprofile/openapi/test/YamlAnswerTest.java
new file mode 100644 (file)
index 0000000..d9215a6
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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.geronimo.microprofile.openapi.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.URL;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.arquillian.testng.Arquillian;
+import org.jboss.shrinkwrap.api.Archive;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.EmptyAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.testng.annotations.Test;
+
+public class YamlAnswerTest extends Arquillian {
+    @Deployment(testable = false)
+    public static Archive<?> war() {
+        return ShrinkWrap.create(WebArchive.class, YamlAnswerTest.class.getSimpleName() + ".war")
+                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
+    }
+
+    @ArquillianResource
+    private URL base;
+
+    @Test
+    public void getYaml() {
+        assertEquals("---\nopenapi: \"3.0.1\"\n", api("text/vnd.yaml"));
+    }
+
+    @Test
+    public void getJson() {
+        assertEquals("{\"openapi\":\"3.0.1\"}", api("application/json"));
+    }
+
+    private String api(final String type) {
+        final Client client = ClientBuilder.newClient();
+        try {
+            return client.target(base.toExternalForm())
+                    .path("openapi")
+                    .request(type)
+                    .get(String.class);
+        } finally {
+            client.close();
+        }
+    }
+}