GERONIMO-6661 OpenAPIMojo tolerance of interface design - not fully generic yet ...
authorRomain Manni-Bucau <rmannibucau@gmail.com>
Mon, 17 Dec 2018 17:50:42 +0000 (18:50 +0100)
committerRomain Manni-Bucau <rmannibucau@gmail.com>
Mon, 17 Dec 2018 17:50:42 +0000 (18:50 +0100)
16 files changed:
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/loader/ApiBindings.java [new file with mode: 0644]
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/loader/DefaultLoader.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/impl/model/APIResponsesImpl.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/model/HeaderImpl.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/model/MediaTypeImpl.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/model/ParameterImpl.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/model/SchemaImpl.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/model/ScopesImpl.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/model/SecuritySchemeImpl.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/model/codec/Serializers.java
geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessor.java
geronimo-openapi-maven-plugin/pom.xml
geronimo-openapi-maven-plugin/src/main/java/org/apache/geronimo/microprofile/openapi/mojo/OpenAPIMojo.java
geronimo-openapi-maven-plugin/src/test/java/org/apache/geronimo/microprofile/openapi/mojo/OpenAPIMojoTest.java [new file with mode: 0644]
pom.xml

diff --git a/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/loader/ApiBindings.java b/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/loader/ApiBindings.java
new file mode 100644 (file)
index 0000000..723b94b
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * 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.impl.loader;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.geronimo.microprofile.openapi.impl.model.APIResponseImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.APIResponsesImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.CallbackImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ComponentsImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ContactImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ContentImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.DiscriminatorImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.EncodingImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ExampleImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ExtensibleImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ExternalDocumentationImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.HeaderImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.InfoImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.LicenseImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.LinkImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.MediaTypeImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.OAuthFlowImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.OAuthFlowsImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.OpenAPIImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.OperationImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ParameterImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.PathItemImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.PathsImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ReferenceImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.RequestBodyImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.SchemaImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ScopesImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.SecurityRequirementImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.SecuritySchemeImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ServerImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ServerVariableImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.ServerVariablesImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.TagImpl;
+import org.apache.geronimo.microprofile.openapi.impl.model.XMLImpl;
+import org.eclipse.microprofile.openapi.models.Components;
+import org.eclipse.microprofile.openapi.models.Extensible;
+import org.eclipse.microprofile.openapi.models.ExternalDocumentation;
+import org.eclipse.microprofile.openapi.models.OpenAPI;
+import org.eclipse.microprofile.openapi.models.Operation;
+import org.eclipse.microprofile.openapi.models.PathItem;
+import org.eclipse.microprofile.openapi.models.Paths;
+import org.eclipse.microprofile.openapi.models.Reference;
+import org.eclipse.microprofile.openapi.models.callbacks.Callback;
+import org.eclipse.microprofile.openapi.models.examples.Example;
+import org.eclipse.microprofile.openapi.models.headers.Header;
+import org.eclipse.microprofile.openapi.models.info.Contact;
+import org.eclipse.microprofile.openapi.models.info.Info;
+import org.eclipse.microprofile.openapi.models.info.License;
+import org.eclipse.microprofile.openapi.models.links.Link;
+import org.eclipse.microprofile.openapi.models.media.Content;
+import org.eclipse.microprofile.openapi.models.media.Discriminator;
+import org.eclipse.microprofile.openapi.models.media.Encoding;
+import org.eclipse.microprofile.openapi.models.media.MediaType;
+import org.eclipse.microprofile.openapi.models.media.Schema;
+import org.eclipse.microprofile.openapi.models.media.XML;
+import org.eclipse.microprofile.openapi.models.parameters.Parameter;
+import org.eclipse.microprofile.openapi.models.parameters.RequestBody;
+import org.eclipse.microprofile.openapi.models.responses.APIResponse;
+import org.eclipse.microprofile.openapi.models.responses.APIResponses;
+import org.eclipse.microprofile.openapi.models.security.OAuthFlow;
+import org.eclipse.microprofile.openapi.models.security.OAuthFlows;
+import org.eclipse.microprofile.openapi.models.security.Scopes;
+import org.eclipse.microprofile.openapi.models.security.SecurityRequirement;
+import org.eclipse.microprofile.openapi.models.security.SecurityScheme;
+import org.eclipse.microprofile.openapi.models.servers.Server;
+import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
+import org.eclipse.microprofile.openapi.models.servers.ServerVariables;
+import org.eclipse.microprofile.openapi.models.tags.Tag;
+
+public class ApiBindings {
+    private ApiBindings() {
+        // no-op
+    }
+
+    public static Map<Class<?>, Class<?>> get() {
+        final Map<Class<?>, Class<?>> mapping = new HashMap<>(33);
+        mapping.put(APIResponse.class, APIResponseImpl.class);
+        mapping.put(APIResponses.class, APIResponsesImpl.class);
+        mapping.put(Callback.class, CallbackImpl.class);
+        mapping.put(Components.class, ComponentsImpl.class);
+        mapping.put(Contact.class, ContactImpl.class);
+        mapping.put(Content.class, ContentImpl.class);
+        mapping.put(Discriminator.class, DiscriminatorImpl.class);
+        mapping.put(Encoding.class, EncodingImpl.class);
+        mapping.put(Example.class, ExampleImpl.class);
+        mapping.put(Extensible.class, ExtensibleImpl.class);
+        mapping.put(ExternalDocumentation.class, ExternalDocumentationImpl.class);
+        mapping.put(Header.class, HeaderImpl.class);
+        mapping.put(Info.class, InfoImpl.class);
+        mapping.put(License.class, LicenseImpl.class);
+        mapping.put(Link.class, LinkImpl.class);
+        mapping.put(MediaType.class, MediaTypeImpl.class);
+        mapping.put(OAuthFlow.class, OAuthFlowImpl.class);
+        mapping.put(OAuthFlows.class, OAuthFlowsImpl.class);
+        mapping.put(OpenAPI.class, OpenAPIImpl.class);
+        mapping.put(Operation.class, OperationImpl.class);
+        mapping.put(Parameter.class, ParameterImpl.class);
+        mapping.put(PathItem.class, PathItemImpl.class);
+        mapping.put(Paths.class, PathsImpl.class);
+        mapping.put(Reference.class, ReferenceImpl.class);
+        mapping.put(RequestBody.class, RequestBodyImpl.class);
+        mapping.put(Schema.class, SchemaImpl.class);
+        mapping.put(Scopes.class, ScopesImpl.class);
+        mapping.put(SecurityRequirement.class, SecurityRequirementImpl.class);
+        mapping.put(SecurityScheme.class, SecuritySchemeImpl.class);
+        mapping.put(Server.class, ServerImpl.class);
+        mapping.put(ServerVariable.class, ServerVariableImpl.class);
+        mapping.put(ServerVariables.class, ServerVariablesImpl.class);
+        mapping.put(Tag.class, TagImpl.class);
+        mapping.put(XML.class, XMLImpl.class);
+        return mapping;
+    }
+}
index ba9ebc4..c5b5e95 100644 (file)
@@ -26,6 +26,7 @@ import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
 import javax.json.bind.Jsonb;
 import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
 import javax.servlet.ServletContext;
 
 import org.apache.geronimo.microprofile.openapi.impl.model.OpenAPIImpl;
@@ -41,7 +42,9 @@ public class DefaultLoader {
         return Stream.of("", "/").map(prefix -> prefix + "META-INF/openapi.json")
                 .map(it -> ofNullable(loader.getResourceAsStream(it)).orElseGet(() -> context.getResourceAsStream(it)))
                 .filter(Objects::nonNull).findFirst().map(r -> {
-                    try (final Jsonb jsonb = JsonbBuilder.create(); final InputStream stream = r) {
+                    try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
+                            .setProperty("johnzon.interfaceImplementationMapping", ApiBindings.get()));
+                         final InputStream stream = r) {
                         return jsonb.fromJson(stream, OpenAPIImpl.class);
                     } catch (final Exception e) {
                         throw new IllegalStateException(e);
index b7124ee..5ba558b 100644 (file)
@@ -27,6 +27,7 @@ import javax.enterprise.inject.Vetoed;
 import javax.json.bind.annotation.JsonbProperty;
 import javax.json.bind.annotation.JsonbTransient;
 
+import org.apache.geronimo.microprofile.openapi.impl.loader.ApiBindings;
 import org.apache.geronimo.microprofile.openapi.impl.model.APIResponseImpl;
 import org.apache.geronimo.microprofile.openapi.impl.model.APIResponsesImpl;
 import org.apache.geronimo.microprofile.openapi.impl.model.CallbackImpl;
@@ -131,40 +132,7 @@ public final class Yaml {
     // 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);
+        ApiBindings.get().forEach((k, v) -> resolver.addMapping(Class.class.cast(k), v));
 
         final SimpleModule module = new SimpleModule();
         module.setAbstractTypes(resolver);
index fe8c46f..55e2a4d 100644 (file)
@@ -29,6 +29,10 @@ public class APIResponsesImpl extends LinkedHashMap<String, APIResponse> impleme
 
     private APIResponse _default;
 
+    public APIResponses addAPIResponse(final String name, final APIResponse item) {
+        return addApiResponse(name, item);
+    }
+
     @Override
     public APIResponses addApiResponse(final String name, final APIResponse item) {
         this.put(name, item);
index bbca62a..e8dc64e 100644 (file)
@@ -250,6 +250,7 @@ public class HeaderImpl implements Header {
     }
 
     @Override
+    @JsonbTypeAdapter(Serializers.HeaderStyleSerializer.class)
     public void setStyle(final Style _style) {
         this._style = _style;
     }
index 067d9d1..d1bfffe 100644 (file)
@@ -20,9 +20,11 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 import javax.enterprise.inject.Vetoed;
+import javax.json.bind.annotation.JsonbTypeAdapter;
 import javax.json.bind.annotation.JsonbTypeDeserializer;
 
 import org.apache.geronimo.microprofile.openapi.impl.model.codec.Deserializers;
+import org.apache.geronimo.microprofile.openapi.impl.model.codec.Serializers;
 import org.eclipse.microprofile.openapi.models.Extensible;
 import org.eclipse.microprofile.openapi.models.examples.Example;
 import org.eclipse.microprofile.openapi.models.media.Encoding;
@@ -42,6 +44,7 @@ public class MediaTypeImpl implements MediaType {
     @JsonbTypeDeserializer(Deserializers.MapExamplesDeserializer.class)
     private Map<String, Example> _examples;
 
+    @JsonbTypeAdapter(Serializers.SchemaTypeSerializer.class)
     private Schema _schema;
 
     @Override
index f93d9dc..3eec8ad 100644 (file)
@@ -219,6 +219,7 @@ public class ParameterImpl implements Parameter {
     }
 
     @Override
+    @JsonbTypeAdapter(Serializers.InSerializer.class)
     public void setIn(final In _in) {
         this._in = _in;
     }
@@ -302,6 +303,7 @@ public class ParameterImpl implements Parameter {
     }
 
     @Override
+    @JsonbTypeAdapter(Serializers.ParameterStyleSerializer.class)
     public void setStyle(final Style _style) {
         this._style = _style;
     }
index 2b84f38..4449375 100644 (file)
@@ -701,6 +701,7 @@ public class SchemaImpl implements Schema {
     }
 
     @Override
+    @JsonbTypeAdapter(Serializers.SchemaTypeSerializer.class)
     public void setType(final SchemaType _type) {
         this._type = _type;
     }
index 5c44b89..6224edc 100644 (file)
@@ -49,4 +49,8 @@ public class ScopesImpl extends LinkedHashMap<String, String> implements Scopes
         this.put(name, item);
         return this;
     }
+
+    public Scopes addString(final String name, final String item) {
+        return addScope(name, item);
+    }
 }
index bdd554b..df7f9bf 100644 (file)
@@ -120,6 +120,7 @@ public class SecuritySchemeImpl implements SecurityScheme {
     }
 
     @Override
+    @JsonbTypeAdapter(Serializers.SecuritySchemeInSerializer.class)
     public void setIn(final In _in) {
         this._in = _in;
     }
@@ -203,6 +204,7 @@ public class SecuritySchemeImpl implements SecurityScheme {
     }
 
     @Override
+    @JsonbTypeAdapter(Serializers.SecuritySchemeTypeSerializer.class)
     public void setType(final Type _type) {
         this._type = _type;
     }
index cbcbba8..761ce4c 100644 (file)
@@ -19,6 +19,7 @@ package org.apache.geronimo.microprofile.openapi.impl.model.codec;
 import static java.util.Locale.ROOT;
 
 import java.math.BigDecimal;
+import java.util.stream.Stream;
 
 import javax.enterprise.inject.Vetoed;
 import javax.json.bind.adapter.JsonbAdapter;
@@ -68,7 +69,13 @@ public final class Serializers {
 
         @Override
         public E adaptFromJson(final String obj) {
-            return Enum.valueOf(type, obj.toUpperCase(ROOT));
+            try {
+                return Enum.valueOf(type, obj.toUpperCase(ROOT));
+            } catch (final IllegalArgumentException iae) {
+                return Stream.of(type.getEnumConstants())
+                             .filter(it -> it.toString().equals(obj)).findFirst()
+                             .orElseThrow(() -> iae);
+            }
         }
     }
 
index 61ad2b6..8123e4b 100644 (file)
@@ -352,6 +352,21 @@ public class AnnotationProcessor {
                         produces.filter(it -> !it.isEmpty()).map(it -> it.iterator().next()).orElse(null), p,
                         api.getComponents(), ofNullable(p.getAnnotation(RequestBody.class))
                     .orElseGet(() -> m.getAnnotation(RequestBody.class)))));
+        if (operation.getResponses() == null) {
+            final APIResponsesImpl responses = new APIResponsesImpl();
+            operation.responses(responses);
+            final boolean normalResponse = Stream.of(m.getParameters()).noneMatch(it -> it.isAnnotationPresent(Suspended.class));
+            final ContentImpl content = new ContentImpl();
+            if (normalResponse) {
+                final MediaType impl = new MediaTypeImpl();
+                impl.setSchema(schemaProcessor.mapSchemaFromClass(api.getComponents(), m.getReturnType()));
+                produces.orElseGet(() -> singletonList("*/*")).forEach(key -> content.put(key, impl));
+            }
+            responses.put(
+                    m.getReturnType() == void.class || m.getReturnType() == Void.class && normalResponse ?
+                            "204" : "200",
+                    new APIResponseImpl().content(content));
+        }
         return operation;
     }
 
@@ -386,7 +401,7 @@ public class AnnotationProcessor {
         return Stream.concat(Stream.of(t.value()), Stream.of(t.refs()).map(TagAnnotation::new));
     }
 
-    private Optional<List<String>> findProduces(AnnotatedMethodElement m) {
+    private Optional<List<String>> findProduces(final AnnotatedMethodElement m) {
         return ofNullable(ofNullable(m.getAnnotation(Produces.class))
                 .orElseGet(() -> m.getDeclaringClass().getAnnotation(Produces.class)))
                 .map(p -> Stream.of(p.value()).collect(toList()));
index 307fe5c..98b989f 100644 (file)
     <dependency>
       <groupId>org.apache.johnzon</groupId>
       <artifactId>johnzon-jsonb</artifactId>
-      <version>1.1.7</version>
+      <version>${johnzon.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 
index 2a1fe6c..1886e95 100644 (file)
@@ -1,6 +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.geronimo.microprofile.openapi.mojo;
 
+import static java.util.Arrays.asList;
 import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
 import static org.apache.maven.plugins.annotations.LifecyclePhase.PROCESS_CLASSES;
 import static org.apache.maven.plugins.annotations.ResolutionScope.COMPILE_PLUS_RUNTIME;
 
@@ -9,6 +29,7 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.io.Writer;
 import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Method;
 import java.lang.reflect.Type;
 import java.net.MalformedURLException;
@@ -16,7 +37,10 @@ import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.LinkedList;
 import java.util.Map;
+import java.util.Objects;
+import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
 import javax.json.bind.Jsonb;
@@ -37,28 +61,28 @@ import org.apache.maven.project.MavenProject;
 @Mojo(name = "openapi.json", defaultPhase = PROCESS_CLASSES, requiresDependencyResolution = COMPILE_PLUS_RUNTIME, threadSafe = true)
 public class OpenAPIMojo extends AbstractMojo {
     @Parameter(property = "geronimo-openapi.skip", defaultValue = "false")
-    private boolean skip;
+    protected boolean skip;
 
     @Parameter(property = "geronimo-openapi.prettify", defaultValue = "true")
-    private boolean prettify;
+    protected boolean prettify;
 
     @Parameter(property = "geronimo-openapi.output", defaultValue = "${project.build.outputDirectory}/META-INF/classes/openapi.json")
-    private File output;
+    protected File output;
 
     @Parameter
-    private String application;
+    protected String application;
 
     @Parameter
-    private Collection<String> endpointClasses;
+    protected Collection<String> endpointClasses;
 
     @Parameter
-    private Map<String, String> configuration;
+    protected Map<String, String> configuration;
 
     @Parameter(defaultValue = "${project.build.outputDirectory}")
-    private File classes;
+    protected File classes;
 
     @Parameter(defaultValue = "${project}", readonly = true)
-    private MavenProject project;
+    protected MavenProject project;
 
     @Override
     public void execute() throws MojoExecutionException {
@@ -72,7 +96,11 @@ public class OpenAPIMojo extends AbstractMojo {
         final ClassLoader pluginLoader = thread.getContextClassLoader();
         try {
             try (final URLClassLoader loader = new URLClassLoader(
-                    Stream.concat(Stream.of(classes), project.getArtifacts().stream().map(Artifact::getFile))
+                    Stream.concat(
+                                Stream.of(classes),
+                                ofNullable(project)
+                                        .map(p -> p.getArtifacts().stream().map(Artifact::getFile)).orElseGet(Stream::empty))
+                            .filter(Objects::nonNull)
                             .map(file -> {
                                 try {
                                     return file.toURI().toURL();
@@ -131,63 +159,95 @@ public class OpenAPIMojo extends AbstractMojo {
         }
     }
 
+    private static Annotation[] mergeAnnotations(final AnnotatedElement... element) {
+        return Stream.of(element)
+                     .flatMap(i -> Stream.of(i.getAnnotations()))
+                     .collect(toMap(Annotation::annotationType, identity(), (a, b) -> a))
+                     .values()
+                     .toArray(new Annotation[0]);
+    }
+
     private static class MethodElement implements AnnotatedMethodElement {
-        private final Method delegate;
+        private static final Method[] NO_METHOD = new Method[0];
+
+        private final Method[] delegates;
+        private Annotation[] annotations;
 
         private MethodElement(final Method method) {
-            this.delegate = method;
+            final Collection<Method> methods = new LinkedList<>();
+            methods.add(method);
+            Stream.of(method.getDeclaringClass().getInterfaces())
+                  .map(it -> {
+                      try {
+                          return it.getMethod(method.getName(), method.getParameterTypes());
+                      } catch (final NoSuchMethodException e) {
+                          return null;
+                      }
+                  })
+                  .filter(Objects::nonNull).forEach(methods::add);
+            this.delegates = methods.toArray(NO_METHOD);
         }
 
         @Override
         public String getName() {
-            return delegate.getName();
+            return delegates[0].getName();
         }
 
         @Override
         public Type getReturnType() {
-            return delegate.getGenericReturnType();
+            return delegates[0].getGenericReturnType();
         }
 
         @Override
         public Class<?> getDeclaringClass() {
-            return delegate.getDeclaringClass();
+            return delegates[0].getDeclaringClass();
         }
 
         @Override
         public AnnotatedTypeElement[] getParameters() {
-            return Stream.of(delegate.getParameters())
-                    .map(p -> new AnnotatedTypeElement() {
-                        @Override
-                        public Type getType() {
-                            return p.getParameterizedType();
-                        }
-
-                        @Override
-                        public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
-                            return p.getAnnotation(annotationClass);
-                        }
-
-                        @Override
-                        public Annotation[] getAnnotations() {
-                            return p.getAnnotations();
-                        }
-
-                        @Override
-                        public Annotation[] getDeclaredAnnotations() {
-                            return p.getDeclaredAnnotations();
-                        }
-                    })
-                    .toArray(AnnotatedTypeElement[]::new);
+            final java.lang.reflect.Parameter[] parameters = delegates[0].getParameters();
+            return IntStream.range(0, parameters.length)
+                        .mapToObj(p -> new AnnotatedTypeElement() {
+                            private Annotation[] annotations;
+
+                            @Override
+                            public Type getType() {
+                                return parameters[p].getParameterizedType();
+                            }
+
+                            @Override // todo:
+                            public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
+                                return Stream.of(delegates)
+                                        .map(m -> m.getParameters()[p])
+                                        .filter(it -> it.isAnnotationPresent(annotationClass))
+                                        .map(it -> it.getAnnotation(annotationClass))
+                                        .findFirst()
+                                        .orElse(null);
+                            }
+
+                            @Override
+                            public Annotation[] getAnnotations() {
+                                return annotations == null ? annotations = mergeAnnotations(delegates) : annotations;
+                            }
+
+                            @Override
+                            public Annotation[] getDeclaredAnnotations() {
+                                return getAnnotations();
+                            }
+                        }).toArray(AnnotatedTypeElement[]::new);
         }
 
         @Override
         public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
-            return delegate.getAnnotation(annotationClass);
+            return Stream.of(delegates)
+                         .filter(d -> d.isAnnotationPresent(annotationClass))
+                         .map(d -> d.getAnnotation(annotationClass))
+                         .findFirst().orElse(null);
         }
 
         @Override
         public Annotation[] getAnnotations() {
-            return delegate.getAnnotations();
+            return annotations == null ? annotations = mergeAnnotations(delegates) : annotations;
         }
 
         @Override
@@ -198,6 +258,7 @@ public class OpenAPIMojo extends AbstractMojo {
 
     private static class ClassElement implements AnnotatedTypeElement {
         private final Class<?> delegate;
+        private Annotation[] annotations;
 
         private ClassElement(final Class<?> delegate) {
             this.delegate = delegate;
@@ -205,22 +266,44 @@ public class OpenAPIMojo extends AbstractMojo {
 
         @Override
         public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
-            return delegate.getAnnotation(annotationClass);
+            return ofNullable(delegate.getAnnotation(annotationClass))
+                    .orElseGet(() -> findInInterfaces(annotationClass));
         }
 
         @Override
         public Annotation[] getAnnotations() {
-            return delegate.getAnnotations();
+            return annotations == null ? annotations = gatherAnnotations() : annotations;
         }
 
         @Override
         public Annotation[] getDeclaredAnnotations() {
-            return delegate.getDeclaredAnnotations();
+            return getAnnotations();
         }
 
         @Override
         public Type getType() {
             return delegate;
         }
+
+        private Annotation[] gatherAnnotations() {
+            final Collection<Annotation> annotations = new LinkedList<>(asList(delegate.getAnnotations()));
+            annotations.addAll(findInterfaces(delegate)
+                    .flatMap(i -> Stream.of(i.getAnnotations()).filter(it -> !delegate.isAnnotationPresent(it.annotationType())))
+                    .distinct()
+                    .collect(toList()));
+            return annotations.toArray(new Annotation[0]);
+        }
+
+        private <T extends Annotation> T findInInterfaces(final Class<T> annotationClass) {
+            return findInterfaces(delegate)
+                    .filter(it -> it.isAnnotationPresent(annotationClass))
+                    .findFirst()
+                    .map(it -> it.getAnnotation(annotationClass))
+                    .orElse(null);
+        }
+
+        private Stream<Class<?>> findInterfaces(final Class<?> delegate) {
+            return Stream.of(delegate.getInterfaces());
+        }
     }
 }
diff --git a/geronimo-openapi-maven-plugin/src/test/java/org/apache/geronimo/microprofile/openapi/mojo/OpenAPIMojoTest.java b/geronimo-openapi-maven-plugin/src/test/java/org/apache/geronimo/microprofile/openapi/mojo/OpenAPIMojoTest.java
new file mode 100644 (file)
index 0000000..ab3e2ea
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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.mojo;
+
+import static java.util.Collections.singleton;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
+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.MediaType;
+
+import org.apache.geronimo.microprofile.openapi.impl.loader.ApiBindings;
+import org.apache.geronimo.microprofile.openapi.impl.model.OpenAPIImpl;
+import org.eclipse.microprofile.openapi.models.OpenAPI;
+import org.eclipse.microprofile.openapi.models.Operation;
+import org.eclipse.microprofile.openapi.models.media.Schema;
+import org.junit.Test;
+
+public class OpenAPIMojoTest {
+    @Test
+    public void run() throws Exception {
+        final OpenAPIMojo mojo = new OpenAPIMojo();
+        mojo.output = new File("target/OpenAPIMojoTest_run_1.json");
+        mojo.endpointClasses = singleton(HelloServiceImpl1.class.getName());
+        mojo.execute();
+        final OpenAPI openAPI = readOpenAPI(mojo.output);
+        final Operation get = openAPI.getPaths().get("/sayHello/{a}").getGET();
+        assertNotNull(get);
+        assertEquals(1, get.getParameters().size());
+        assertEquals("a", get.getParameters().iterator().next().getName());
+        assertEquals(Schema.SchemaType.STRING, get.getResponses().get("200").getContent().get("text/plain").getSchema().getType());
+    }
+
+    private OpenAPI readOpenAPI(final File output) throws Exception {
+        try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().setProperty("johnzon.interfaceImplementationMapping", ApiBindings.get()));
+             final InputStream stream = new FileInputStream(output)) {
+            return jsonb.fromJson(stream, OpenAPIImpl.class);
+        }
+    }
+
+    @Path("/sayHello")
+    public interface HelloService {
+        @GET
+        @Path("/{a}")
+        @Produces(MediaType.TEXT_PLAIN)
+        String hi(@PathParam("a") String a);
+
+    }
+
+    public static class HelloServiceImpl1 implements HelloService {
+
+        public String hi(String a) {
+            return "";
+        }
+
+    }
+}
diff --git a/pom.xml b/pom.xml
index 3fc8c3b..4ddcc03 100644 (file)
--- a/pom.xml
+++ b/pom.xml
     <arquillian.version>1.1.14.Final</arquillian.version>
     <testng.version>6.9.9</testng.version>
     <jackson.version>2.9.4</jackson.version>
+    <johnzon.version>1.1.11-SNAPSHOT</johnzon.version>
   </properties>
 
   <dependencyManagement>
     <dependencies>
       <dependency>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-core</artifactId>
+        <version>${johnzon.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-mapper</artifactId>
+        <version>${johnzon.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-jsonb</artifactId>
+        <version>${johnzon.version}</version>
+      </dependency>
+      <dependency>
         <groupId>org.eclipse.microprofile.openapi</groupId>
         <artifactId>microprofile-openapi-api</artifactId>
         <version>${spec.version}</version>