[GERONIMO-6786] @BeanParam basic support
diff --git a/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessor.java b/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessor.java
index a4b9315..ecb7d13 100644
--- a/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessor.java
+++ b/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessor.java
@@ -50,6 +50,7 @@
 import javax.json.JsonReaderFactory;
 import javax.json.JsonValue;
 import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.BeanParam;
 import javax.ws.rs.CookieParam;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -96,6 +97,8 @@
 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.processor.reflect.ClassElement;
+import org.apache.geronimo.microprofile.openapi.impl.processor.reflect.FieldElement;
 import org.apache.geronimo.microprofile.openapi.impl.processor.spi.NamingStrategy;
 import org.eclipse.microprofile.openapi.OASConfig;
 import org.eclipse.microprofile.openapi.annotations.Components;
@@ -385,9 +388,9 @@
         });
         operation.parameters(Stream.of(m.getParameters())
                 .filter(it -> it.isAnnotationPresent(Parameter.class) || hasJaxRsParams(it))
-                .map(it -> buildParameter(it, api)
-                        .orElseGet(() -> new ParameterImpl().schema(schemaProcessor.mapSchemaFromClass(
-                                () -> getOrCreateComponents(api), it.getType()))))
+                .flatMap(it -> buildParameter(it, api)
+                        .orElseGet(() -> Stream.of(new ParameterImpl().schema(schemaProcessor.mapSchemaFromClass(
+                                () -> getOrCreateComponents(api), it.getType())))))
                 .filter(Objects::nonNull).collect(toList()));
         Stream.of(m.getParameters())
                 .filter(it -> it.isAnnotationPresent(Parameters.class))
@@ -507,42 +510,73 @@
 
     private boolean hasJaxRsParams(final AnnotatedElement it) {
         return it.isAnnotationPresent(HeaderParam.class) || it.isAnnotationPresent(CookieParam.class) ||
-                it.isAnnotationPresent(PathParam.class) || it.isAnnotationPresent(QueryParam.class);
+                it.isAnnotationPresent(PathParam.class) || it.isAnnotationPresent(QueryParam.class) ||
+                it.isAnnotationPresent(BeanParam.class);
     }
 
-    private Optional<org.eclipse.microprofile.openapi.models.parameters.Parameter> buildParameter(
+    private Optional<Stream<org.eclipse.microprofile.openapi.models.parameters.Parameter>> buildParameter(
             final AnnotatedTypeElement annotatedElement, final OpenAPI openAPI) {
         return ofNullable(ofNullable(annotatedElement.getAnnotation(Parameter.class))
                 .map(it -> mapParameter(annotatedElement, () -> getOrCreateComponents(openAPI), it))
+                .map(Stream::of)
                 .orElseGet(() -> {
                     if (hasJaxRsParams(annotatedElement)) {
-                        final ParameterImpl parameter = new ParameterImpl();
-                        if (annotatedElement.isAnnotationPresent(HeaderParam.class)) {
-                            parameter.in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.HEADER)
-                                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.SIMPLE)
-                                    .name(annotatedElement.getAnnotation(HeaderParam.class).value());
-                        } else if (annotatedElement.isAnnotationPresent(CookieParam.class)) {
-                            parameter.in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.COOKIE)
-                                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.FORM)
-                                    .name(annotatedElement.getAnnotation(CookieParam.class).value());
-                        } else if (annotatedElement.isAnnotationPresent(PathParam.class)) {
-                            parameter.required(true)
-                                    .in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.PATH)
-                                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.SIMPLE)
-                                    .name(annotatedElement.getAnnotation(PathParam.class).value());
-                        } else if (annotatedElement.isAnnotationPresent(QueryParam.class)) {
-                            parameter.in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.QUERY)
-                                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.FORM)
-                                    .name(annotatedElement.getAnnotation(QueryParam.class).value());
+                        if (annotatedElement.isAnnotationPresent(BeanParam.class)) {
+                            return fromBeanParam(annotatedElement, openAPI);
                         }
-                        parameter.schema(schemaProcessor.mapSchemaFromClass(
-                                    () -> getOrCreateComponents(openAPI), annotatedElement.getType()));
-                        return parameter;
+                        return Stream.of(bindParam(annotatedElement, openAPI));
                     }
-                    return null;
+                    return Stream.empty();
                 }));
     }
 
+    private Stream<org.eclipse.microprofile.openapi.models.parameters.Parameter> fromBeanParam(
+            final AnnotatedTypeElement elt, final OpenAPI openAPI) {
+        final Type type = elt.getType();
+        if (type != null && type != Object.class) {
+            final Class<?> clazz = Class.class.cast(type);
+            return Stream.concat(fromBeanParamForType(clazz, openAPI), fromBeanParamForType(clazz.getSuperclass(), openAPI));
+        }
+        return null;
+    }
+
+    private Stream<org.eclipse.microprofile.openapi.models.parameters.Parameter> fromBeanParamForType(
+            final Class<?> type, final OpenAPI openAPI) {
+        if (type == null || type == Object.class) {
+            return Stream.empty();
+        }
+        return Stream.of(type.getDeclaredFields())
+                .filter(this::hasJaxRsParams)
+                .map(FieldElement::new)
+                .map(it -> bindParam(it, openAPI));
+    }
+
+    private ParameterImpl bindParam(final AnnotatedTypeElement annotatedElement,
+                                    final OpenAPI openAPI) {
+        final ParameterImpl parameter = new ParameterImpl();
+        if (annotatedElement.isAnnotationPresent(HeaderParam.class)) {
+            parameter.in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.HEADER)
+                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.SIMPLE)
+                    .name(annotatedElement.getAnnotation(HeaderParam.class).value());
+        } else if (annotatedElement.isAnnotationPresent(CookieParam.class)) {
+            parameter.in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.COOKIE)
+                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.FORM)
+                    .name(annotatedElement.getAnnotation(CookieParam.class).value());
+        } else if (annotatedElement.isAnnotationPresent(PathParam.class)) {
+            parameter.required(true)
+                    .in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.PATH)
+                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.SIMPLE)
+                    .name(annotatedElement.getAnnotation(PathParam.class).value());
+        } else if (annotatedElement.isAnnotationPresent(QueryParam.class)) {
+            parameter.in(org.eclipse.microprofile.openapi.models.parameters.Parameter.In.QUERY)
+                    .style(org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.FORM)
+                    .name(annotatedElement.getAnnotation(QueryParam.class).value());
+        }
+        parameter.schema(schemaProcessor.mapSchemaFromClass(
+                    () -> getOrCreateComponents(openAPI), annotatedElement.getType()));
+        return parameter;
+    }
+
     private PathItem getPathItem(final OpenAPI api, final String path) {
         return api.getPaths().computeIfAbsent(path, p -> {
             final PathItemImpl item = new PathItemImpl();
diff --git a/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/reflect/FieldElement.java b/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/reflect/FieldElement.java
new file mode 100644
index 0000000..39f41b2
--- /dev/null
+++ b/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/reflect/FieldElement.java
@@ -0,0 +1,52 @@
+/*
+ * 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.processor.reflect;
+
+import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotatedTypeElement;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+
+public class FieldElement implements AnnotatedTypeElement {
+    private final Field delegate;
+    private Annotation[] annotations;
+
+    public FieldElement(final Field delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
+        return delegate.getAnnotation(annotationClass);
+    }
+
+    @Override
+    public Annotation[] getAnnotations() {
+        return delegate.getAnnotations();
+    }
+
+    @Override
+    public Annotation[] getDeclaredAnnotations() {
+        return delegate.getDeclaredAnnotations();
+    }
+
+    @Override
+    public Type getType() {
+        return delegate.getGenericType();
+    }
+}
diff --git a/geronimo-openapi-impl/src/test/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessorTest.java b/geronimo-openapi-impl/src/test/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessorTest.java
index 378859d..8d2b733 100644
--- a/geronimo-openapi-impl/src/test/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessorTest.java
+++ b/geronimo-openapi-impl/src/test/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotationProcessorTest.java
@@ -23,14 +23,18 @@
 import org.apache.geronimo.microprofile.openapi.impl.processor.spi.NamingStrategy;
 import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 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.parameters.Parameter;
 import org.eclipse.microprofile.openapi.models.responses.APIResponses;
 import org.testng.annotations.Test;
 
 import javax.json.JsonPatch;
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.CookieParam;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
 import javax.ws.rs.PATCH;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
@@ -38,12 +42,12 @@
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
+import static java.util.stream.Collectors.joining;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 
@@ -62,7 +66,7 @@
         assertEquals("a", parameters.get(0).getName());
         // TODO add more assertions
     }
-    
+
     @Test
     public void ensureParameterAnnotationsAreMerged() {
         AnnotationProcessor annotationProcessor = new AnnotationProcessor(GeronimoOpenAPIConfig.create(), new NamingStrategy.Default(), null);
@@ -75,7 +79,7 @@
         assertEquals(Parameter.In.QUERY, parameters.get(0).getIn());
         assertEquals("b", parameters.get(0).getName());
     }
-    
+
     @Test
     public void ensureResponsesMediaTypeIsSetForDefaultResponses() {
         AnnotationProcessor annotationProcessor = new AnnotationProcessor(GeronimoOpenAPIConfig.create(), new NamingStrategy.Default(), null);
@@ -91,7 +95,7 @@
         assertNotNull(responses.get("204"));
         assertNotNull(responses.get("204").getContent().get("text/plain"));
     }
-    
+
     @Test
     public void ensureResponsesMediaTypeIsSetForAllResponses() {
         AnnotationProcessor annotationProcessor = new AnnotationProcessor(GeronimoOpenAPIConfig.create(), new NamingStrategy.Default(), null);
@@ -107,7 +111,7 @@
         assertNotNull(responses.get("204"));
         assertNotNull(responses.get("204").getContent().get("application/json"));
     }
-    
+
     @Test
     public void ensureResponsesDefaultMediaTypeIsSet() {
         AnnotationProcessor annotationProcessor = new AnnotationProcessor(GeronimoOpenAPIConfig.create(), new NamingStrategy.Default(), null);
@@ -159,6 +163,20 @@
         assertNotNull(openAPI.getPaths().get("/{a}").getPATCH().getOperationId()); // we didn't get an index exception
     }
 
+    @Test
+    public void beanParam() {
+        final AnnotationProcessor annotationProcessor = new AnnotationProcessor((value, def) -> null, new NamingStrategy.Default(), null);
+        final OpenAPI openAPI = new OpenAPIImpl();
+        annotationProcessor.processClass("", openAPI, new ClassElement(Patched.class),
+                Stream.of(TestResource.class.getMethods()).map(MethodElement::new));
+        final Operation get = openAPI.getPaths().getPathItem("/beanparam").getGET();
+        assertNotNull(get);
+        assertEquals(2, get.getParameters().size());
+        assertEquals("header<=first(string),cookie<=second(string)", get.getParameters().stream()
+                .map(it -> it.getIn() + "<=" + it.getName() + "(" + it.getSchema().getType() + ")")
+                .collect(joining(",")));
+    }
+
     @Path("/")
     public class Patched {
 
@@ -190,19 +208,19 @@
         public String hello(@PathParam("a") String a) {
             return "hello";
         }
-        
+
         @GET
         @Path("/bye")
         @Produces(MediaType.TEXT_PLAIN)
         public void bye(@org.eclipse.microprofile.openapi.annotations.parameters.Parameter(required = true) @QueryParam("b") String b) {
         }
-        
+
         @DELETE
         @Path("/bye")
         @APIResponse(responseCode = "204")
         public void bye() {
         }
-        
+
         @PATCH
         @Path("/bye")
         @Produces(MediaType.APPLICATION_JSON)
@@ -211,5 +229,19 @@
         public Response bye(JsonPatch patch) {
             return Response.ok().build();
         }
+
+        @GET
+        @Path("/beanparam")
+        public Response beanParam(@BeanParam final Bound param) {
+            return Response.ok().build();
+        }
+    }
+
+    public static class Bound {
+        @HeaderParam("first")
+        private String premiere;
+
+        @CookieParam("second")
+        private String two;
     }
 }