From a34da046d6ec9a4b9aac5be61d7641e936b936e6 Mon Sep 17 00:00:00 2001 From: Duc Nguyen <6222905+geniegeist@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:21:54 +0100 Subject: [PATCH 1/4] Add support for multiple paths --- .../runtime/scanner/ResourceParameters.java | 28 +- .../spi/AbstractAnnotationScanner.java | 8 + .../spi/AbstractParameterProcessor.java | 602 +++++++++--------- .../openapi/jaxrs/JaxRsAnnotationScanner.java | 23 +- .../jaxrs/JaxRsParameterProcessor.java | 15 +- .../spring/SpringAnnotationScanner.java | 71 ++- .../spring/SpringParameterProcessor.java | 53 +- .../openapi/vertx/VertxAnnotationScanner.java | 4 +- .../vertx/VertxParameterProcessor.java | 14 +- 9 files changed, 437 insertions(+), 381 deletions(-) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java index 733422d73..0e030ead1 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.eclipse.microprofile.openapi.models.media.Content; import org.eclipse.microprofile.openapi.models.media.Schema; @@ -50,10 +51,10 @@ public static Comparator parameterComparator(List preferre static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\{(\\w[\\w\\.-]*)\\}"); - private String pathItemPath; + private List pathItemPaths; private List pathItemParameters; - private String operationPath; + private List operationPaths; private List operationParameters; private Content formBodyContent; @@ -62,12 +63,14 @@ public List getPathItemParameters() { return pathItemParameters; } - public String getOperationPath() { - return operationPath; + public List getOperationPaths() { + return operationPaths; } - public String getFullOperationPath() { - return pathItemPath + operationPath; + public List getFullOperationPaths() { + return pathItemPaths.stream() + .flatMap(pathItemPath -> operationPaths.stream().map(operationPath -> pathItemPath + operationPath)) + .collect(Collectors.toList()); } public List getOperationParameters() { @@ -107,16 +110,16 @@ public List getAllParameters() { return all; } - public void setPathItemPath(String pathItemPath) { - this.pathItemPath = pathItemPath; + public void setPathItemPaths(List pathItemPaths) { + this.pathItemPaths = pathItemPaths; } public void setPathItemParameters(List pathItemParameters) { this.pathItemParameters = pathItemParameters; } - public void setOperationPath(String operationPath) { - this.operationPath = operationPath; + public void setOperationPaths(List operationPaths) { + this.operationPaths = operationPaths; } public void setOperationParameters(List operationParameters) { @@ -139,7 +142,10 @@ public void sort(List preferredOrder) { } public List getPathParameterTemplateNames() { - return getPathParameterTemplateName(this.pathItemPath, this.operationPath); + return pathItemPaths.stream() + .flatMap(pathItemPath -> operationPaths.stream() + .flatMap(operationPath -> getPathParameterTemplateName(pathItemPath, operationPath).stream())) + .collect(Collectors.toList()); } private static List getPathParameterTemplateName(String... paths) { diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java index 307e8bdc1..037084590 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java @@ -7,7 +7,9 @@ import java.math.BigInteger; import java.nio.file.Path; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.microprofile.openapi.models.Extensible; import org.jboss.jandex.DotName; @@ -63,6 +65,12 @@ public void setContextRoot(String path) { this.contextRoot = path; } + protected List makePaths(List operationPaths) { + return operationPaths.stream() + .map(operationPath -> createPathFromSegments(this.contextRoot, this.currentAppPath, operationPath)) + .collect(Collectors.toList()); + } + protected String makePath(String operationPath) { return createPathFromSegments(this.contextRoot, this.currentAppPath, operationPath); } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index 818d9a43e..f95477b5d 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -69,11 +69,9 @@ */ public abstract class AbstractParameterProcessor { + protected static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded"; private static Set openApiParameterAnnotations = new HashSet<>( Arrays.asList(Names.PARAMETER, Names.PARAMETERS)); - - protected static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded"; - protected final AnnotationScannerContext scannerContext; protected final String contextPath; protected final IndexView index; @@ -110,122 +108,180 @@ public abstract class AbstractParameterProcessor { private Set processedMatrixSegments = new HashSet<>(); private List preferredOrder; + protected AbstractParameterProcessor(AnnotationScannerContext scannerContext, + String contextPath, + Function reader, + List extensions) { + this.scannerContext = scannerContext; + this.contextPath = contextPath; + this.index = scannerContext.getIndex(); + this.readerFunction = reader; + this.extensions = extensions; + this.beanValidationScanner = scannerContext.getBeanValidationScanner(); + } + /** - * Used for collecting and merging any scanned {@link Parameter} annotations - * with the framework-specific parameter annotations. After scanning, this object may - * contain either the MP-OAI annotation information, the framework's annotation - * information, or both. + * Check if the given parameter name is present as a path segment in the resourcePath. * - * @author Michael Edgar {@literal } + * @param paramName name of parameter + * @param paramStyle style of parameter, e.g. simple or matrix + * @param resourcePath resource path/URL + * @return true if the paramName is in the resourcePath, false otherwise. */ - protected static class ParameterContext { - protected String name; - protected In location; - protected Style style; - protected Parameter oaiParam; - protected FrameworkParameter frameworkParam; - protected Object defaultValue; - protected AnnotationTarget target; - protected Type targetType; - - ParameterContext() { + static boolean parameterInPath(String paramName, Style paramStyle, String resourcePath) { + if (paramName == null || resourcePath == null) { + return true; } - public ParameterContext(String name, FrameworkParameter frameworkParam, AnnotationTarget target, Type targetType) { - this.name = name; - this.location = frameworkParam.location; - this.style = frameworkParam.style; - this.frameworkParam = frameworkParam; - this.target = target; - this.targetType = targetType; - } + final String regex; - @Override - public String toString() { - return "name: " + name + "; in: " + location + "; target: " + target; + if (Style.MATRIX.equals(paramStyle)) { + regex = String.format("(?:\\{[ \\t]*|^|/?)\\Q%s\\E(?:[ \\t]*(?:}|:)|/?|$)", paramName); + } else { + regex = String.format("\\{[ \\t]*\\Q%s\\E[ \\t]*(?:}|:)", paramName); } + + return Pattern.compile(regex).matcher(resourcePath).find(); } /** - * Key used to store {@link ParameterContext} objects in a map sorted by {@link In}, - * then by name, nulls first. - * - * @author Michael Edgar {@literal } + * Check if the given parameter name is present as a path segment in the resourcePath. * + * @param paramName name of parameter + * @param paramStyle style of parameter, e.g. simple or matrix + * @param resourcePath resource path/URL + * @return true if the paramName is in the resourcePath, false otherwise. */ - protected static class ParameterContextKey { - final String name; - final In location; - final Style style; - final String ref; + static boolean parameterInPaths(String paramName, Style paramStyle, List resourcePaths) { + return resourcePaths.stream() + .anyMatch(resourcePath -> parameterInPath(paramName, paramStyle, resourcePath)); + } - public ParameterContextKey(String name, In location, Style style) { - this.name = name; - this.location = location; - this.style = style; - this.ref = null; + /** + * Retrieves either the provided parameter {@link Parameter.Style}, the default + * style of the parameter based on the in attribute, or null if in is not defined. + * + * @param param the {@link Parameter} + * @return the param's style, the default style defined based on in, or null if in is not defined. + */ + protected static Style styleOf(Parameter param) { + if (param.getStyle() != null) { + return param.getStyle(); } - public ParameterContextKey(Parameter oaiParam) { - this.name = oaiParam.getName(); - this.location = oaiParam.getIn(); - this.style = styleOf(oaiParam); - this.ref = oaiParam.getRef(); + if (param.getIn() != null) { + switch (param.getIn()) { + case COOKIE: + case QUERY: + return Style.FORM; + case HEADER: + case PATH: + return Style.SIMPLE; + default: + break; + } } - public ParameterContextKey(ParameterContext context) { - this.name = context.name; - this.location = context.location; - this.style = context.style; - this.ref = context.oaiParam != null ? context.oaiParam.getRef() : null; + return null; + } + + /** + * Retrieves the "value" parameter from annotation to be used as the name. + * If no value was specified or an empty value, return the name of the annotation + * target. + * + * @param annotation parameter annotation + * @return the name of the parameter + */ + protected static String paramName(AnnotationInstance annotation) { + AnnotationValue value = annotation.value(); + String valueString = null; + + if (value != null) { + valueString = value.asString(); + if (valueString.length() > 0) { + return valueString; + } } - @Override - public boolean equals(Object obj) { - if (obj instanceof ParameterContextKey) { - ParameterContextKey other = (ParameterContextKey) obj; + AnnotationTarget target = annotation.target(); - if (isNull() && other.isNull()) { - return this == other; - } + switch (target.kind()) { + case FIELD: + valueString = target.asField().name(); + break; + case METHOD_PARAMETER: + valueString = target.asMethodParameter().name(); + break; + case METHOD: + // This is a bean property setter + MethodInfo method = target.asMethod(); + if (method.parametersCount() == 1) { + String methodName = method.name(); - if (ref != null) { - return ref.equals(other.ref); + if (methodName.startsWith("set")) { + valueString = Introspector.decapitalize(methodName.substring(3)); + } else { + valueString = methodName; + } } + break; + default: + break; + } - return Objects.equals(this.name, other.name) && - Objects.equals(this.location, other.location) && - Objects.equals(this.style, other.style); - } + return valueString; + } - return false; + /** + * Determines the type of the target. Method annotations will give + * the name of a single argument, assumed to be a "setter" method. + * + * @param target target object + * @return object type + */ + protected static Type getType(AnnotationTarget target) { + if (target == null) { + return null; } - @Override - public int hashCode() { - return isNull() ? super.hashCode() : Objects.hash(name, location, style, ref); - } + Type type = null; - @Override - public String toString() { - return "name: " + name + "; in: " + location + "; style: " + style + "; ref: " + ref; + switch (target.kind()) { + case FIELD: + type = target.asField().type(); + break; + case METHOD: + List methodParams = target.asMethod().parameterTypes(); + if (methodParams.size() == 1) { + // This is a bean property setter + type = methodParams.get(0); + } + break; + case METHOD_PARAMETER: + type = target.asMethodParameter().method().parameterType(target.asMethodParameter().position()); + break; + default: + break; } - public boolean isNull() { - return name == null && location == null && style == null && ref == null; - } + return type; } - protected AbstractParameterProcessor(AnnotationScannerContext scannerContext, - String contextPath, - Function reader, - List extensions) { - this.scannerContext = scannerContext; - this.contextPath = contextPath; - this.index = scannerContext.getIndex(); - this.readerFunction = reader; - this.extensions = extensions; - this.beanValidationScanner = scannerContext.getBeanValidationScanner(); + /** + * Obtain the MethodInfo associated with the annotation target. + * + * @param target annotated item. Only method and method parameter targets. + * @return the MethodInfo associated with the target, or null if target is not a method or parameter. + */ + protected static MethodInfo targetMethod(AnnotationTarget target) { + if (target.kind() == Kind.METHOD) { + return target.asMethod(); + } + if (target.kind() == Kind.METHOD_PARAMETER) { + return target.asMethodParameter().method(); + } + return null; } protected void reset() { @@ -293,8 +349,8 @@ protected void processFinalize(ClassInfo resourceClass, MethodInfo resourceMetho * Generate the path using the provided resource class, which may differ from the method's declaring * class - e.g. for inheritance. */ - parameters.setPathItemPath(generatePath(resourceClass, allParameters)); - parameters.setOperationPath(generatePath(resourceMethod, allParameters)); + parameters.setPathItemPaths(generatePaths(resourceClass, allParameters)); + parameters.setOperationPaths(generatePaths(resourceMethod, allParameters)); parameters.getPathParameterTemplateNames() .stream() @@ -311,18 +367,24 @@ protected void processFinalize(ClassInfo resourceClass, MethodInfo resourceMetho } /** - * Generate the path for the provided annotation target, either a class or a method. + * Generate the paths for the provided annotation target, either a class or a method. * Add the name of any discovered matrix parameters. * * @param target the target (either class or method) * @param parameters list of all parameters processed - * @return the path for the target + * @return the paths for the target */ - protected String generatePath(AnnotationTarget target, List parameters) { - final StringBuilder path = new StringBuilder(pathOf(target)); + protected List generatePaths(AnnotationTarget target, List parameters) { + return pathsOf(target).stream() + .map(path -> generatePath(path, parameters)) + .collect(Collectors.toList()); + } + + private String generatePath(String path, List parameters) { + final StringBuilder pathBuilder = new StringBuilder(path); - if (path.length() > 0) { - path.insert(0, '/'); + if (pathBuilder.length() > 0) { + pathBuilder.insert(0, '/'); } /* @@ -330,7 +392,7 @@ protected String generatePath(AnnotationTarget target, List parameter * is specified, extract the pattern and apply to the parameter's schema * if no pattern is otherwise specified and the parameter is a string. */ - Matcher templateMatcher = getTemplateParameterPattern().matcher(path); + Matcher templateMatcher = getTemplateParameterPattern().matcher(pathBuilder); while (templateMatcher.find()) { String variableName = templateMatcher.group(1).trim(); @@ -342,16 +404,16 @@ protected String generatePath(AnnotationTarget target, List parameter .forEach(p -> p.getSchema().setPattern(variablePattern)); String replacement = templateMatcher.replaceFirst('{' + variableName + '}'); - path.setLength(0); - path.append(replacement); + pathBuilder.setLength(0); + pathBuilder.append(replacement); - templateMatcher = getTemplateParameterPattern().matcher(path); + templateMatcher = getTemplateParameterPattern().matcher(pathBuilder); } parameters.stream() .filter(p -> Style.MATRIX.equals(p.getStyle())) .filter(p -> !processedMatrixSegments.contains(p.getName())) - .filter(p -> path.indexOf(p.getName()) > -1) + .filter(p -> pathBuilder.indexOf(p.getName()) > -1) .forEach(matrix -> { String segmentName = matrix.getName(); processedMatrixSegments.add(segmentName); @@ -359,24 +421,24 @@ protected String generatePath(AnnotationTarget target, List parameter String matrixRef = '{' + segmentName + '}'; int insertIndex = -1; - if ((insertIndex = path.lastIndexOf(matrixRef)) > -1) { + if ((insertIndex = pathBuilder.lastIndexOf(matrixRef)) > -1) { insertIndex += matrixRef.length(); // Path already contains a variable of same name, the matrix must be renamed String generatedName = segmentName + "Matrix"; matrix.setName(generatedName); matrixRef = '{' + generatedName + '}'; - } else if ((insertIndex = path.lastIndexOf(segmentName)) > -1) { + } else if ((insertIndex = pathBuilder.lastIndexOf(segmentName)) > -1) { insertIndex += segmentName.length(); } if (insertIndex > -1) { - path.insert(insertIndex, matrixRef); + pathBuilder.insert(insertIndex, matrixRef); } else { ScannerSPILogging.log.missingPathSegment(segmentName); } }); - return path.toString(); + return pathBuilder.toString(); } protected abstract Pattern getTemplateParameterPattern(); @@ -878,7 +940,7 @@ protected boolean isIgnoredParameter(Parameter parameter, AnnotationTarget resou return true; } - if (paramIn == In.PATH && !parameterInPath(paramName, parameter.getStyle(), fullPathOf(resourceMethod))) { + if (paramIn == In.PATH && !parameterInPaths(paramName, parameter.getStyle(), fullPathsOf(resourceMethod))) { return true; } @@ -895,30 +957,6 @@ protected boolean isIgnoredParameter(Parameter parameter, AnnotationTarget resou return false; } - /** - * Check if the given parameter name is present as a path segment in the resourcePath. - * - * @param paramName name of parameter - * @param paramStyle style of parameter, e.g. simple or matrix - * @param resourcePath resource path/URL - * @return true if the paramName is in the resourcePath, false otherwise. - */ - static boolean parameterInPath(String paramName, Style paramStyle, String resourcePath) { - if (paramName == null || resourcePath == null) { - return true; - } - - final String regex; - - if (Style.MATRIX.equals(paramStyle)) { - regex = String.format("(?:\\{[ \\t]*|^|/?)\\Q%s\\E(?:[ \\t]*(?:}|:)|/?|$)", paramName); - } else { - regex = String.format("\\{[ \\t]*\\Q%s\\E[ \\t]*(?:}|:)", paramName); - } - - return Pattern.compile(regex).matcher(resourcePath).find(); - } - /** * Read a single annotation that is either {@link org.eclipse.microprofile.openapi.annotations.parameters.Parameter * {@literal @}Parameter} or @@ -981,34 +1019,6 @@ protected void readAnnotatedType(AnnotationInstance annotation) { protected abstract void readAnnotatedType(AnnotationInstance annotation, AnnotationInstance beanParamAnnotation, boolean overriddenParametersOnly); - /** - * Retrieves either the provided parameter {@link Parameter.Style}, the default - * style of the parameter based on the in attribute, or null if in is not defined. - * - * @param param the {@link Parameter} - * @return the param's style, the default style defined based on in, or null if in is not defined. - */ - protected static Style styleOf(Parameter param) { - if (param.getStyle() != null) { - return param.getStyle(); - } - - if (param.getIn() != null) { - switch (param.getIn()) { - case COOKIE: - case QUERY: - return Style.FORM; - case HEADER: - case PATH: - return Style.SIMPLE; - default: - break; - } - } - - return null; - } - /** * Set this {@link AbstractParameterProcessor}'s formMediaType if it has not already * been set and the value is explicitly known for the parameter type. @@ -1022,54 +1032,6 @@ protected void setMediaType(FrameworkParameter frameworkParam) { } } - /** - * Retrieves the "value" parameter from annotation to be used as the name. - * If no value was specified or an empty value, return the name of the annotation - * target. - * - * @param annotation parameter annotation - * @return the name of the parameter - */ - protected static String paramName(AnnotationInstance annotation) { - AnnotationValue value = annotation.value(); - String valueString = null; - - if (value != null) { - valueString = value.asString(); - if (valueString.length() > 0) { - return valueString; - } - } - - AnnotationTarget target = annotation.target(); - - switch (target.kind()) { - case FIELD: - valueString = target.asField().name(); - break; - case METHOD_PARAMETER: - valueString = target.asMethodParameter().name(); - break; - case METHOD: - // This is a bean property setter - MethodInfo method = target.asMethod(); - if (method.parametersCount() == 1) { - String methodName = method.name(); - - if (methodName.startsWith("set")) { - valueString = Introspector.decapitalize(methodName.substring(3)); - } else { - valueString = methodName; - } - } - break; - default: - break; - } - - return valueString; - } - protected Set getDefaultAnnotationNames() { return Collections.emptySet(); } @@ -1151,52 +1113,60 @@ protected Object primitiveToObject(Primitive primitive, String stringValue) { } /** - * Retrieves the last path segment of the full path associated with the target. If - * the last path segment contains a path variable name, returns the variable name. + * Retrieves all last path segments of all full paths associated with the target. If + * a last path segment contains a path variable name, it returns the variable name. * * @param target - * @return the last path segment of the target, or null if no path is defined + * @return the last path segments of the target, or null if no path is defined */ - protected String lastPathSegmentOf(AnnotationTarget target) { - String fullPath = fullPathOf(target); - String lastSegment = null; + protected List lastPathSegmentsOf(AnnotationTarget target) { + List fullPaths = fullPathsOf(target); + List lastSegments = null; - if (fullPath != null) { - lastSegment = fullPath.substring(fullPath.lastIndexOf('/') + 1); + if (fullPaths != null && !fullPaths.isEmpty()) { + lastSegments = fullPaths.stream() + .map(fullPath -> { + String lastSegment = fullPath.substring(fullPath.lastIndexOf('/') + 1); - if (lastSegment.startsWith("{") && lastSegment.endsWith("}")) { - lastSegment = lastSegment.substring(1, lastSegment.length() - 1); - } + if (lastSegment.startsWith("{") && lastSegment.endsWith("}")) { + lastSegment = lastSegment.substring(1, lastSegment.length() - 1); + } + + return lastSegment; + }) + .collect(Collectors.toList()); } - return lastSegment; + return lastSegments; } /** - * Find the full path of the target, including parent resources if the annotation target is a member of a sub-resource - * class. Method-level targets will include both the path to the resource and the path to the method joined with a '/'. + * Find the full paths of the target, including parent resources if the annotation target is a member of a sub-resource + * class. Method-level targets will include both the paths to the resource and the paths to the method joined with a '/'. * * @param target target item for which the path is being generated - * @return full path (excluding application path) of the target + * @return full paths (excluding application path) of the target */ - protected String fullPathOf(AnnotationTarget target) { - String pathSegment = null; + protected List fullPathsOf(AnnotationTarget target) { + List pathSegments = List.of("null"); switch (target.kind()) { case FIELD: - pathSegment = pathOf(target.asField().declaringClass()); + pathSegments = pathsOf(target.asField().declaringClass()); break; case METHOD: - pathSegment = methodPath(target.asMethod()); + pathSegments = methodPaths(target.asMethod()); break; case METHOD_PARAMETER: - pathSegment = methodPath(target.asMethodParameter().method()); + pathSegments = methodPaths(target.asMethodParameter().method()); break; default: break; } - return contextPath + '/' + pathSegment; + return pathSegments.stream() + .map(pathSegment -> contextPath + '/' + pathSegment) + .collect(Collectors.toList()); } /** @@ -1205,61 +1175,29 @@ protected String fullPathOf(AnnotationTarget target) { * * @param method the method annotated with the framework's path annotation */ - String methodPath(MethodInfo method) { - String methodPath = pathOf(method); - String classPath = pathOf(method.declaringClass()); + List methodPaths(MethodInfo method) { + List methodPaths = pathsOf(method); + List classPaths = pathsOf(method.declaringClass()); - if (methodPath.isEmpty()) { - return classPath; + if (methodPaths.isEmpty()) { + return classPaths; } - return classPath + '/' + methodPath; + return classPaths.stream() + .flatMap(classPath -> methodPaths.stream() + .map(methodPath -> methodPath.isEmpty() ? classPath : classPath + '/' + methodPath)) + .collect(Collectors.toList()); } /** - * Reads the framework's path annotation present on the + * Reads the framework's path annotations present on the * target and strips leading and trailing slashes. * * @param target target object - * @return value of the framework's path annotation without + * @return value of the framework's path annotations without * leading/trailing slashes. */ - protected abstract String pathOf(AnnotationTarget target); - - /** - * Determines the type of the target. Method annotations will give - * the name of a single argument, assumed to be a "setter" method. - * - * @param target target object - * @return object type - */ - protected static Type getType(AnnotationTarget target) { - if (target == null) { - return null; - } - - Type type = null; - - switch (target.kind()) { - case FIELD: - type = target.asField().type(); - break; - case METHOD: - List methodParams = target.asMethod().parameterTypes(); - if (methodParams.size() == 1) { - // This is a bean property setter - type = methodParams.get(0); - } - break; - case METHOD_PARAMETER: - type = target.asMethodParameter().method().parameterType(target.asMethodParameter().position()); - break; - default: - break; - } - - return type; - } + protected abstract List pathsOf(AnnotationTarget target); protected boolean isReadableParameterAnnotation(DotName name) { return Names.PARAMETER.equals(name) && readerFunction != null; @@ -1461,22 +1399,6 @@ boolean nameAndStyleMatch(ParameterContext context, ParameterContextKey key) { return false; } - /** - * Obtain the MethodInfo associated with the annotation target. - * - * @param target annotated item. Only method and method parameter targets. - * @return the MethodInfo associated with the target, or null if target is not a method or parameter. - */ - protected static MethodInfo targetMethod(AnnotationTarget target) { - if (target.kind() == Kind.METHOD) { - return target.asMethod(); - } - if (target.kind() == Kind.METHOD_PARAMETER) { - return target.asMethodParameter().method(); - } - return null; - } - /** * Scans for class level parameters on the given class argument and its ancestors. * @@ -1605,4 +1527,110 @@ protected boolean hasParameters(Collection annotations) { } protected abstract boolean isParameter(DotName annotationName); + + /** + * Used for collecting and merging any scanned {@link Parameter} annotations + * with the framework-specific parameter annotations. After scanning, this object may + * contain either the MP-OAI annotation information, the framework's annotation + * information, or both. + * + * @author Michael Edgar {@literal } + */ + protected static class ParameterContext { + protected String name; + protected In location; + protected Style style; + protected Parameter oaiParam; + protected FrameworkParameter frameworkParam; + protected Object defaultValue; + protected AnnotationTarget target; + protected Type targetType; + + ParameterContext() { + } + + public ParameterContext(String name, FrameworkParameter frameworkParam, AnnotationTarget target, Type targetType) { + this.name = name; + this.location = frameworkParam.location; + this.style = frameworkParam.style; + this.frameworkParam = frameworkParam; + this.target = target; + this.targetType = targetType; + } + + @Override + public String toString() { + return "name: " + name + "; in: " + location + "; target: " + target; + } + } + + /** + * Key used to store {@link ParameterContext} objects in a map sorted by {@link In}, + * then by name, nulls first. + * + * @author Michael Edgar {@literal } + * + */ + protected static class ParameterContextKey { + final String name; + final In location; + final Style style; + final String ref; + + public ParameterContextKey(String name, In location, Style style) { + this.name = name; + this.location = location; + this.style = style; + this.ref = null; + } + + public ParameterContextKey(Parameter oaiParam) { + this.name = oaiParam.getName(); + this.location = oaiParam.getIn(); + this.style = styleOf(oaiParam); + this.ref = oaiParam.getRef(); + } + + public ParameterContextKey(ParameterContext context) { + this.name = context.name; + this.location = context.location; + this.style = context.style; + this.ref = context.oaiParam != null ? context.oaiParam.getRef() : null; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ParameterContextKey) { + ParameterContextKey other = (ParameterContextKey) obj; + + if (isNull() && other.isNull()) { + return this == other; + } + + if (ref != null) { + return ref.equals(other.ref); + } + + return Objects.equals(this.name, other.name) && + Objects.equals(this.location, other.location) && + Objects.equals(this.style, other.style); + } + + return false; + } + + @Override + public int hashCode() { + return isNull() ? super.hashCode() : Objects.hash(name, location, style, ref); + } + + @Override + public String toString() { + return "name: " + name + "; in: " + location + "; style: " + style + "; ref: " + ref; + } + + public boolean isNull() { + return name == null && location == null && style == null && ref == null; + } + } } diff --git a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java index efb826b96..45fce06d1 100644 --- a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java +++ b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java @@ -422,12 +422,10 @@ private void processSubResource(final ClassInfo resourceClass, final String originalAppPath = this.currentAppPath; final String subResourcePath; - if (this.subResourceStack.isEmpty()) { - subResourcePath = params.getFullOperationPath(); - } else { - // If we are already processing a sub-resource, ignore any @Path information from the current class - subResourcePath = params.getOperationPath(); - } + List operationPaths = this.subResourceStack.isEmpty() ? params.getFullOperationPaths() + : params.getOperationPaths(); + boolean operationsPathIsEmpty = operationPaths == null || operationPaths.isEmpty(); + subResourcePath = !operationsPathIsEmpty ? operationPaths.get(0) : null; this.currentAppPath = createPathFromSegments(this.currentAppPath, subResourcePath); this.subResourceStack.push(locator); @@ -537,15 +535,12 @@ private void processResourceMethod(final ClassInfo resourceClass, return; } + // When processing a sub-resource tree, ignore any @Path information from the current class + List operationPaths = this.subResourceStack.isEmpty() ? params.getFullOperationPaths() + : params.getOperationPaths(); + boolean operationsPathIsEmpty = operationPaths == null || operationPaths.isEmpty(); // Figure out the path for the operation. This is a combination of the App, Resource, and Method @Path annotations - final String path; - - if (this.subResourceStack.isEmpty()) { - path = super.makePath(params.getFullOperationPath()); - } else { - // When processing a sub-resource tree, ignore any @Path information from the current class - path = super.makePath(params.getOperationPath()); - } + final String path = super.makePath(!operationsPathIsEmpty ? operationPaths.get(0) : null); // Get or create a PathItem to hold the operation PathItem existingPath = ModelUtil.paths(context.getOpenApi()).getPathItem(path); diff --git a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java index 15ef92424..f1eea79a0 100644 --- a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java +++ b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java @@ -188,10 +188,11 @@ private void readJaxRsParameter(AnnotationInstance annotation, readFrameworkParameter(annotation, frameworkParam, overriddenParametersOnly); } else if (frameworkParam.style == Style.MATRIX) { // Store the @MatrixParam for later processing - String pathSegment = beanParamAnnotation != null - ? lastPathSegmentOf(beanParamAnnotation.target()) - : lastPathSegmentOf(target); - + List pathSegments = beanParamAnnotation != null + ? lastPathSegmentsOf(beanParamAnnotation.target()) + : lastPathSegmentsOf(target); + boolean isPathSegmentsEmpty = pathSegments == null || pathSegments.isEmpty(); + String pathSegment = !isPathSegmentsEmpty ? pathSegments.get(0) : null; matrixParams.computeIfAbsent(pathSegment, k -> new HashMap<>()) .put(paramName(annotation), annotation); } else if (frameworkParam.location == In.PATH && targetType != null @@ -225,7 +226,7 @@ protected String getDefaultAnnotationProperty() { } @Override - protected String pathOf(AnnotationTarget target) { + protected List pathsOf(AnnotationTarget target) { AnnotationInstance path = null; String pathValue = null; @@ -255,10 +256,10 @@ protected String pathOf(AnnotationTarget target) { pathValue = pathValue.substring(0, pathValue.length() - 1); } - return pathValue; + return List.of(pathValue); } - return ""; + return List.of(""); } AnnotationInstance pathOf(MethodInfo method) { diff --git a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java index f3864ecf0..75c5e190d 100644 --- a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java +++ b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.microprofile.openapi.OASFactory; @@ -166,8 +167,10 @@ private void processControllerClasses(OpenAPI openApi) { processScannerExtensions(context, applications); for (ClassInfo controller : applications) { - OpenAPI applicationOpenApi = processControllerClass(controller); - openApi = MergeUtil.merge(openApi, applicationOpenApi); + List applicationOpenApis = processControllerClass(controller); + for (OpenAPI applicationOpenApi : applicationOpenApis) { + openApi = MergeUtil.merge(openApi, applicationOpenApi); + } } } @@ -179,41 +182,45 @@ private void processControllerClasses(OpenAPI openApi) { * @param context the scanning context * @param controllerClass the Spring REST controller */ - private OpenAPI processControllerClass(ClassInfo controllerClass) { + private List processControllerClass(ClassInfo controllerClass) { SpringLogging.log.processingController(controllerClass.simpleName()); - TypeResolver resolver = TypeResolver.forClass(context, controllerClass, null); - context.getResolverStack().push(resolver); - // Get the @RequestMapping info and save it for later AnnotationInstance requestMappingAnnotation = context.annotations().getAnnotation(controllerClass, SpringConstants.REQUEST_MAPPING); - if (requestMappingAnnotation != null) { - this.currentAppPath = SpringParameterProcessor.requestMappingValuesToPath(requestMappingAnnotation); - } else { - this.currentAppPath = "/"; - } + List appPaths = requestMappingAnnotation != null + ? SpringParameterProcessor.requestMappingValuesToPath(requestMappingAnnotation) + : List.of("/"); - // Process @OpenAPIDefinition annotation - OpenAPI openApi = processDefinitionAnnotation(context, controllerClass); + return appPaths.stream() + .map(path -> { + TypeResolver resolver = TypeResolver.forClass(context, controllerClass, null); + context.getResolverStack().push(resolver); - // Process @SecurityScheme annotations - processSecuritySchemeAnnotation(context, controllerClass, openApi); + this.currentAppPath = path; - // Process @Server annotations - processServerAnnotation(context, controllerClass, openApi); + // Process @OpenAPIDefinition annotation + OpenAPI openApi = processDefinitionAnnotation(context, controllerClass); - // Process Java security - processJavaSecurity(context, controllerClass, openApi); + // Process @SecurityScheme annotations + processSecuritySchemeAnnotation(context, controllerClass, openApi); - // Now find and process the operation methods - processControllerMethods(controllerClass, openApi, null); + // Process @Server annotations + processServerAnnotation(context, controllerClass, openApi); - context.getResolverStack().pop(); + // Process Java security + processJavaSecurity(context, controllerClass, openApi); - return openApi; + // Now find and process the operation methods + processControllerMethods(controllerClass, openApi, null); + + context.getResolverStack().pop(); + + return openApi; + }) + .collect(Collectors.toList()); } @Override @@ -327,16 +334,18 @@ private void processControllerMethod(final ClassInfo resourceClass, } // Figure out the path for the operation. This is a combination of the App, Resource, and Method @Path annotations - String path = super.makePath(params.getOperationPath()); + List paths = super.makePaths(params.getOperationPaths()); - // Get or create a PathItem to hold the operation - PathItem existingPath = ModelUtil.paths(openApi).getPathItem(path); + for (String path : paths) { + // Get or create a PathItem to hold the operation + PathItem existingPath = ModelUtil.paths(openApi).getPathItem(path); - if (existingPath == null) { - ModelUtil.paths(openApi).addPathItem(path, pathItem); - } else { - // Changes applied to 'existingPath', no need to re-assign or add to OAI. - MergeUtil.mergeObjects(existingPath, pathItem); + if (existingPath == null) { + ModelUtil.paths(openApi).addPathItem(path, pathItem); + } else { + // Changes applied to 'existingPath', no need to re-assign or add to OAI. + MergeUtil.mergeObjects(existingPath, pathItem); + } } } diff --git a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java index 4af136cf9..e75ee7f4e 100644 --- a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java +++ b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java @@ -6,6 +6,7 @@ import java.util.Set; import java.util.function.Function; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.eclipse.microprofile.openapi.models.PathItem; import org.eclipse.microprofile.openapi.models.parameters.Parameter; @@ -107,12 +108,15 @@ protected void readAnnotatedType(AnnotationInstance annotation, AnnotationInstan readFrameworkParameter(annotation, frameworkParam, overriddenParametersOnly); } else if (frameworkParam.style == Style.MATRIX) { // Store the @MatrixParam for later processing - String pathSegment = beanParamAnnotation != null - ? lastPathSegmentOf(beanParamAnnotation.target()) - : lastPathSegmentOf(target); - - matrixParams.computeIfAbsent(pathSegment, k -> new HashMap<>()) - .put(paramName(annotation), annotation); + List pathSegments = beanParamAnnotation != null + ? lastPathSegmentsOf(beanParamAnnotation.target()) + : lastPathSegmentsOf(target); + + for (String pathSegment : pathSegments) { + matrixParams + .computeIfAbsent(pathSegment, k -> new HashMap<>()) + .put(paramName(annotation), annotation); + } // Do this in Spring ? //}else if (frameworkParam.location == In.PATH && targetType != null @@ -168,7 +172,7 @@ protected String getDefaultAnnotationProperty() { } @Override - protected String pathOf(AnnotationTarget target) { + protected List pathsOf(AnnotationTarget target) { AnnotationInstance path = null; Set paths = SpringConstants.HTTP_METHODS; @@ -190,19 +194,24 @@ protected String pathOf(AnnotationTarget target) { } if (path != null) { - String pathValue = requestMappingValuesToPath(path); - if (pathValue.startsWith("/")) { - pathValue = pathValue.substring(1); - } + List pathValues = requestMappingValuesToPath(path); - if (pathValue.endsWith("/")) { - pathValue = pathValue.substring(0, pathValue.length() - 1); - } + return pathValues.stream() + .map(pathValue -> { + if (pathValue.startsWith("/")) { + pathValue = pathValue.substring(1); + } - return pathValue; + if (pathValue.endsWith("/")) { + pathValue = pathValue.substring(0, pathValue.length() - 1); + } + + return pathValue; + }) + .collect(Collectors.toList()); } - return ""; + return List.of(""); } static boolean mappingHasPath(AnnotationInstance mappingAnnotation) { @@ -215,16 +224,12 @@ static boolean mappingHasPath(AnnotationInstance mappingAnnotation) { * @param requestMappingAnnotation * @return */ - static String requestMappingValuesToPath(AnnotationInstance requestMappingAnnotation) { - StringBuilder sb = new StringBuilder(); + static List requestMappingValuesToPath(AnnotationInstance requestMappingAnnotation) { AnnotationValue value = getRequestMappingPathAnnotation(requestMappingAnnotation); - if (value != null) { - String[] parts = value.asStringArray(); - for (String part : parts) { - sb.append(part); - } + if (value == null) { + return Collections.emptyList(); } - return sb.toString(); + return List.of(value.asStringArray()); } static AnnotationValue getRequestMappingPathAnnotation(AnnotationInstance requestMappingAnnotation) { diff --git a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java index 04533fa73..817de8b0b 100644 --- a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java +++ b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java @@ -294,7 +294,9 @@ private void processRouteMethod(final ClassInfo resourceClass, } // Figure out the path for the operation. This is a combination of the App, Resource, and Method @Path annotations - String path = super.makePath(params.getOperationPath()); + List operationPaths = params.getOperationPaths(); + boolean isOperationPathsEmpty = operationPaths == null || operationPaths.isEmpty(); + String path = super.makePath(!isOperationPathsEmpty ? operationPaths.get(0) : null); // Get or create a PathItem to hold the operation PathItem existingPath = ModelUtil.paths(openApi).getPathItem(path); diff --git a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java index 8cd7ba89f..5b30fc48a 100644 --- a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java +++ b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java @@ -126,9 +126,11 @@ private void readAnnotatedType(FrameworkParameter frameworkParam, AnnotationInst readFrameworkParameter(annotation, frameworkParam, overriddenParametersOnly); } else if (frameworkParam.style == Style.MATRIX) { // Store the @MatrixParam for later processing - String pathSegment = beanParamAnnotation != null - ? lastPathSegmentOf(beanParamAnnotation.target()) - : lastPathSegmentOf(target); + List pathSegments = beanParamAnnotation != null + ? lastPathSegmentsOf(beanParamAnnotation.target()) + : lastPathSegmentsOf(target); + boolean isPathSegmentsEmpty = pathSegments == null || pathSegments.isEmpty(); + String pathSegment = !isPathSegmentsEmpty ? pathSegments.get(0) : null; matrixParams.computeIfAbsent(pathSegment, k -> new HashMap<>()) .put(paramName(annotation), annotation); @@ -148,7 +150,7 @@ private void readAnnotatedType(FrameworkParameter frameworkParam, AnnotationInst } @Override - protected String pathOf(AnnotationTarget target) { + protected List pathsOf(AnnotationTarget target) { String pathValue = null; if (target.kind().equals(CLASS)) { @@ -165,7 +167,7 @@ protected String pathOf(AnnotationTarget target) { } if (pathValue == null) { - return ""; + return List.of(""); } if (pathValue.startsWith("/")) { @@ -189,7 +191,7 @@ protected String pathOf(AnnotationTarget target) { pathValue = String.join("/", partsConverted.toArray(new String[] {})); } - return pathValue; + return List.of(pathValue); } @Override From ecae794c8dbfc83e0d79747dbc66288bb92da5dc Mon Sep 17 00:00:00 2001 From: Duc Nguyen <6222905+geniegeist@users.noreply.github.com> Date: Fri, 27 Dec 2024 14:55:43 +0100 Subject: [PATCH 2/4] Add tests for multiple paths --- .../scanner/SpringAnnotationScannerTest.java | 36 +++ .../resources/GreetingDeleteController.java | 6 + .../GreetingDeleteControllerAlt.java | 6 + .../resources/GreetingGetController.java | 5 + .../resources/GreetingGetControllerAlt.java | 6 + .../resources/GreetingGetControllerAlt2.java | 6 + .../GreetingPostControllerMultiplePaths.java | 42 +++ .../GreetingPutControllerMultiplePaths.java | 44 +++ ...stBasicSpringDeleteDefinitionScanning.json | 40 +++ ....testBasicSpringGetDefinitionScanning.json | 52 ++++ ...iplePathsSpringPostDefinitionScanning.json | 225 ++++++++++++++ ...tiplePathsSpringPutDefinitionScanning.json | 289 ++++++++++++++++++ 12 files changed, 757 insertions(+) create mode 100644 extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPostControllerMultiplePaths.java create mode 100644 extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPutControllerMultiplePaths.java create mode 100644 extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPostDefinitionScanning.json create mode 100644 extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPutDefinitionScanning.json diff --git a/extension-spring/src/test/java/io/smallrye/openapi/runtime/scanner/SpringAnnotationScannerTest.java b/extension-spring/src/test/java/io/smallrye/openapi/runtime/scanner/SpringAnnotationScannerTest.java index bf9aa44c7..8299b98ae 100644 --- a/extension-spring/src/test/java/io/smallrye/openapi/runtime/scanner/SpringAnnotationScannerTest.java +++ b/extension-spring/src/test/java/io/smallrye/openapi/runtime/scanner/SpringAnnotationScannerTest.java @@ -16,8 +16,10 @@ import test.io.smallrye.openapi.runtime.scanner.resources.GreetingGetControllerAlt2; import test.io.smallrye.openapi.runtime.scanner.resources.GreetingPostController; import test.io.smallrye.openapi.runtime.scanner.resources.GreetingPostControllerAlt; +import test.io.smallrye.openapi.runtime.scanner.resources.GreetingPostControllerMultiplePaths; import test.io.smallrye.openapi.runtime.scanner.resources.GreetingPutController; import test.io.smallrye.openapi.runtime.scanner.resources.GreetingPutControllerAlt; +import test.io.smallrye.openapi.runtime.scanner.resources.GreetingPutControllerMultiplePaths; /** * Basic Spring annotation scanning @@ -115,6 +117,23 @@ void testBasicPostSpringDefinitionScanningAlt() throws IOException, JSONExceptio assertJsonEquals("resource.testBasicSpringPostDefinitionScanning.json", result); } + /** + * This tests multiple paths + * + * @throws IOException + * @throws JSONException + */ + @Test + void testMultiplePathsPostSpringDefinitionScanningAlt() throws IOException, JSONException { + Index i = indexOf(GreetingPostControllerMultiplePaths.class, Greeting.class, GreetingParam.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), i); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("resource.testMultiplePathsSpringPostDefinitionScanning.json", result); + } + /** * This test a basic, no OpenApi annotations, hello world service * @@ -187,6 +206,23 @@ void testBasicPutSpringDefinitionScanningAlt() throws IOException, JSONException assertJsonEquals("resource.testBasicSpringPutDefinitionScanning.json", result); } + /** + * This tests multiple paths + * + * @throws IOException + * @throws JSONException + */ + @Test + void testMultiplePathsPutSpringDefinitionScanningAlt() throws IOException, JSONException { + Index i = indexOf(GreetingPutControllerMultiplePaths.class, Greeting.class, GreetingParam.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), i); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("resource.testMultiplePathsSpringPutDefinitionScanning.json", result); + } + /** * This test a basic, no OpenApi annotations, hello world service * diff --git a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteController.java b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteController.java index 78c000c72..9dc4875f1 100644 --- a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteController.java +++ b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteController.java @@ -42,4 +42,10 @@ public ResponseEntity greetWithResponse(@PathVariable(name = "id") String public ResponseEntity greetWithResponseTyped(@PathVariable(name = "id") String id) { return ResponseEntity.noContent().build(); } + + // 4) Multiple paths var test + @DeleteMapping(value = { "/multipleGreet1/{id}", "/multipleGreet2/{id}" }) + public void multipleGreet(@PathVariable(name = "id") String id) { + // No op + } } diff --git a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteControllerAlt.java b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteControllerAlt.java index a3f58495f..2bfc8ce47 100644 --- a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteControllerAlt.java +++ b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteControllerAlt.java @@ -43,4 +43,10 @@ public ResponseEntity greetWithResponse(@PathVariable(name = "id") String public ResponseEntity greetWithResponseTyped(@PathVariable(name = "id") String id) { return ResponseEntity.noContent().build(); } + + // 4) Multiple paths var test + @RequestMapping(value = { "/multipleGreet1/{id}", "/multipleGreet2/{id}" }, method = RequestMethod.DELETE) + public void multipleGreet(@PathVariable(name = "id") String id) { + // No op + } } diff --git a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetController.java b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetController.java index 832862945..4fae93c1c 100644 --- a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetController.java +++ b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetController.java @@ -85,4 +85,9 @@ public String overrideProduces(@PathVariable(name = "name") String name) { return "Hello " + name; } + // 8) Multiple paths test + @GetMapping(value = { "/helloMultiplePathsVariable/{name}", "/helloMultiplePathsVariable2/{name}" }) + public Greeting helloMultiplePathsVariable(@PathVariable(name = "name") String name) { + return new Greeting("Hello " + name); + } } diff --git a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt.java b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt.java index 6c02f9a5e..fabbef1c5 100644 --- a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt.java +++ b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt.java @@ -88,4 +88,10 @@ public String overrideProduces(@PathVariable(name = "name") String name) { return "Hello " + name; } + // 8) Multiple paths test + @RequestMapping(value = { "/helloMultiplePathsVariable/{name}", + "/helloMultiplePathsVariable2/{name}" }, method = RequestMethod.GET) + public Greeting helloMultiplePathsVariable(@PathVariable(name = "name") String name) { + return new Greeting("Hello " + name); + } } diff --git a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt2.java b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt2.java index 40537ab3d..37d50eebe 100644 --- a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt2.java +++ b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt2.java @@ -87,4 +87,10 @@ public String overrideProduces(@PathVariable(name = "name") String name) { return "Hello " + name; } + // 8) Multiple paths test + @RequestMapping(path = { "/helloMultiplePathsVariable/{name}", + "/helloMultiplePathsVariable2/{name}" }, method = RequestMethod.GET) + public Greeting helloMultiplePathsVariable(@PathVariable(name = "name") String name) { + return new Greeting("Hello " + name); + } } diff --git a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPostControllerMultiplePaths.java b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPostControllerMultiplePaths.java new file mode 100644 index 000000000..f8c0d7398 --- /dev/null +++ b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPostControllerMultiplePaths.java @@ -0,0 +1,42 @@ +package test.io.smallrye.openapi.runtime.scanner.resources; + +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import test.io.smallrye.openapi.runtime.scanner.entities.Greeting; + +/** + * Spring. + * This class tests if multiple paths are correctly implemented for post operations. + */ +@RestController +@RequestMapping(value = { "/greeting", + "/hello" }, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) +public class GreetingPostControllerMultiplePaths { + + // 1) Basic path var test + @PostMapping({ "/greet1", "/greet2" }) + public Greeting greet(@RequestBody Greeting greeting) { + return greeting; + } + + // 2) ResponseEntity without a type specified + @PostMapping("/greetWithResponse") + @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(ref = "#/components/schemas/Greeting"))) + public ResponseEntity greetWithResponse(@RequestBody Greeting greeting) { + return ResponseEntity.ok(greeting); + } + + // 3) ResponseEntity with a type specified (No JaxRS comparison) + @PostMapping("/greetWithResponseTyped") + public ResponseEntity greetWithResponseTyped(@RequestBody Greeting greeting) { + return ResponseEntity.ok(greeting); + } +} diff --git a/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPutControllerMultiplePaths.java b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPutControllerMultiplePaths.java new file mode 100644 index 000000000..16e24110c --- /dev/null +++ b/extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingPutControllerMultiplePaths.java @@ -0,0 +1,44 @@ +package test.io.smallrye.openapi.runtime.scanner.resources; + +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import test.io.smallrye.openapi.runtime.scanner.entities.Greeting; + +/** + * Spring. + * This class tests if multiple paths are correctly implemented for put operations. + */ +@RestController +@RequestMapping(value = { "/greeting", + "hello" }, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) +public class GreetingPutControllerMultiplePaths { + + // 1) Basic path var test + @PutMapping({ "/greet1/{id}", "/greet2/{id}" }) + public Greeting greet(@RequestBody Greeting greeting, @PathVariable(name = "id") String id) { + return greeting; + } + + // 2) ResponseEntity without a type specified + @PutMapping("/greetWithResponse/{id}") + @APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(ref = "#/components/schemas/Greeting"))) + public ResponseEntity greetWithResponse(@RequestBody Greeting greeting, @PathVariable(name = "id") String id) { + return ResponseEntity.ok(greeting); + } + + // 3) ResponseEntity with a type specified (No JaxRS comparison) + @PutMapping("/greetWithResponseTyped/{id}") + public ResponseEntity greetWithResponseTyped(@RequestBody Greeting greeting, + @PathVariable(name = "id") String id) { + return ResponseEntity.ok(greeting); + } +} diff --git a/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringDeleteDefinitionScanning.json b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringDeleteDefinitionScanning.json index 2114f16f5..244a154c8 100644 --- a/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringDeleteDefinitionScanning.json +++ b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringDeleteDefinitionScanning.json @@ -67,6 +67,46 @@ "oauth" : [ "roles:removal" ] } ] } + }, + "/greeting/multipleGreet1/{id}" : { + "delete" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + } + }, + "security" : [ { + "oauth" : [ "roles:removal" ] + } ] + } + }, + "/greeting/multipleGreet2/{id}" : { + "delete" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + } + }, + "security" : [ { + "oauth" : [ "roles:removal" ] + } ] + } } } } diff --git a/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringGetDefinitionScanning.json b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringGetDefinitionScanning.json index 77ae17d48..78192ac37 100644 --- a/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringGetDefinitionScanning.json +++ b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testBasicSpringGetDefinitionScanning.json @@ -217,6 +217,58 @@ } } } + }, + "/greeting/helloMultiplePathsVariable/{name}" : { + "get": { + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/greeting/helloMultiplePathsVariable2/{name}": { + "get": { + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Greeting" + } + } + } + } + } + } } } } diff --git a/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPostDefinitionScanning.json b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPostDefinitionScanning.json new file mode 100644 index 000000000..62d35b146 --- /dev/null +++ b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPostDefinitionScanning.json @@ -0,0 +1,225 @@ +{ + "openapi" : "3.1.0", + "paths" : { + "/greeting/greet1" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/greeting/greet2" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/greeting/greetWithResponse" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/greeting/greetWithResponseTyped" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greet1" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greet2" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greetWithResponse" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greetWithResponseTyped" : { + "post" : { + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + } + }, + "components" : { + "schemas" : { + "Greeting" : { + "type" : "object", + "properties" : { + "message" : { + "type" : "string" + } + } + } + } + } +} diff --git a/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPutDefinitionScanning.json b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPutDefinitionScanning.json new file mode 100644 index 000000000..1f79fb171 --- /dev/null +++ b/extension-spring/src/test/resources/io/smallrye/openapi/runtime/scanner/resource.testMultiplePathsSpringPutDefinitionScanning.json @@ -0,0 +1,289 @@ +{ + "openapi" : "3.1.0", + "paths" : { + "/greeting/greet1/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/greeting/greet2/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/greeting/greetWithResponse/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/greeting/greetWithResponseTyped/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greet1/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greet2/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greetWithResponse/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + }, + "/hello/greetWithResponseTyped/{id}" : { + "put" : { + "parameters" : [ { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "required": true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Greeting" + } + } + } + } + } + } + } + }, + "components" : { + "schemas" : { + "Greeting" : { + "type" : "object", + "properties" : { + "message" : { + "type" : "string" + } + } + } + } + } +} From 0603753fe0f840a203761fd62cff1b0b0c1e0a64 Mon Sep 17 00:00:00 2001 From: Duc Nguyen <6222905+geniegeist@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:18:22 +0100 Subject: [PATCH 3/4] Collect distinct paths Co-authored-by: Michael Edgar --- .../io/smallrye/openapi/runtime/scanner/ResourceParameters.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java index 0e030ead1..b8403ea55 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java @@ -145,6 +145,7 @@ public List getPathParameterTemplateNames() { return pathItemPaths.stream() .flatMap(pathItemPath -> operationPaths.stream() .flatMap(operationPath -> getPathParameterTemplateName(pathItemPath, operationPath).stream())) + .distinct() .collect(Collectors.toList()); } From f4aac3b3f44679edfbdd22c0cd5d3a6ce2e006ac Mon Sep 17 00:00:00 2001 From: Duc Nguyen <6222905+geniegeist@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:56:00 +0100 Subject: [PATCH 4/4] Improve semantics of null path segment --- .../runtime/scanner/spi/AbstractParameterProcessor.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index f95477b5d..7625e396b 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -1148,7 +1148,7 @@ protected List lastPathSegmentsOf(AnnotationTarget target) { * @return full paths (excluding application path) of the target */ protected List fullPathsOf(AnnotationTarget target) { - List pathSegments = List.of("null"); + List pathSegments = null; switch (target.kind()) { case FIELD: @@ -1164,7 +1164,9 @@ protected List fullPathsOf(AnnotationTarget target) { break; } - return pathSegments.stream() + return Optional.ofNullable(pathSegments) + .orElse(List.of("null")) + .stream() .map(pathSegment -> contextPath + '/' + pathSegment) .collect(Collectors.toList()); }