Skip to content

Commit 4982e1f

Browse files
committed
support spring exchange annotations (#439)
1 parent 34ab453 commit 4982e1f

File tree

8 files changed

+546
-89
lines changed

8 files changed

+546
-89
lines changed

src/main/kotlin/io/openapiprocessor/spring/processor/SpringFrameworkAnnotations.kt

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -112,89 +112,3 @@ private fun getMappingAnnotationName(mappingName: String): String {
112112
private fun getAnnotationName(name: String): String {
113113
return "${ANNOTATION_PKG}.${name}"
114114
}
115-
116-
// To avoid a dependency on Spring, the map provides the http status enum names.
117-
// This may break if the enum name does not exist in the used Spring version.
118-
119-
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
120-
121-
private val HTTP_STATUS = hashMapOf(
122-
"100" to getEnum("CONTINUE"),
123-
"101" to getEnum("SWITCHING_PROTOCOLS"),
124-
"102" to getEnum("PROCESSING"), // WebDAV
125-
"103" to getEnum("CHECKPOINT"),
126-
127-
"200" to getEnum("OK"),
128-
"201" to getEnum("CREATED"),
129-
"202" to getEnum("ACCEPTED"),
130-
"203" to getEnum("NON_AUTHORITATIVE_INFORMATION"),
131-
"204" to getEnum("NO_CONTENT"),
132-
"205" to getEnum("RESET_CONTENT"),
133-
"206" to getEnum("PARTIAL_CONTENT"),
134-
"207" to getEnum("MULTI_STATUS"), // WebDAV
135-
"208" to getEnum("ALREADY_REPORTED"), // WebDAV
136-
"226" to getEnum("IM_USED"),
137-
138-
"300" to getEnum("MULTIPLE_CHOICES"),
139-
"301" to getEnum("MOVED_PERMANENTLY"),
140-
"302" to getEnum("MOVED_TEMPORARILY"),
141-
//"302" to getEnum("FOUND"), // replaces MOVED_TEMPORARILY
142-
"303" to getEnum("SEE_OTHER"),
143-
"304" to getEnum("NOT_MODIFIED"),
144-
"305" to getEnum("USE_PROXY"),
145-
"307" to getEnum("TEMPORARY_REDIRECT"),
146-
"308" to getEnum("PERMANENT_REDIRECT"),
147-
148-
"400" to getEnum("BAD_REQUEST"),
149-
"401" to getEnum("UNAUTHORIZED"),
150-
"402" to getEnum("PAYMENT_REQUIRED"),
151-
"403" to getEnum("FORBIDDEN"),
152-
"404" to getEnum("NOT_FOUND"),
153-
"405" to getEnum("METHOD_NOT_ALLOWED"),
154-
"406" to getEnum("NOT_ACCEPTABLE"),
155-
"407" to getEnum("PROXY_AUTHENTICATION_REQUIRED"),
156-
"408" to getEnum("REQUEST_TIMEOUT"),
157-
"409" to getEnum("CONFLICT"),
158-
"410" to getEnum("GONE"),
159-
"411" to getEnum("LENGTH_REQUIRED"),
160-
"412" to getEnum("PRECONDITION_FAILED"),
161-
"413" to getEnum("REQUEST_ENTITY_TOO_LARGE"),
162-
//"413" to getEnum("PAYLOAD_TOO_LARGE"), // replaces REQUEST_ENTITY_TOO_LARGE
163-
"414" to getEnum("REQUEST_URI_TOO_LONG"),
164-
//"414" to getEnum("URI_TOO_LONG"), // replaces REQUEST_URI_TOO_LONG
165-
"415" to getEnum("UNSUPPORTED_MEDIA_TYPE"),
166-
"416" to getEnum("REQUESTED_RANGE_NOT_SATISFIABLE"),
167-
"417" to getEnum("EXPECTATION_FAILED"),
168-
"418" to getEnum("I_AM_A_TEAPOT"),
169-
"419" to getEnum("INSUFFICIENT_SPACE_ON_RESOURCE"), // WebDAV
170-
"420" to getEnum("METHOD_FAILURE"), // WebDAV
171-
"421" to getEnum("DESTINATION_LOCKED"), // WebDAV
172-
"422" to getEnum("UNPROCESSABLE_ENTITY"), // WebDAV
173-
"423" to getEnum("LOCKED"), // WebDAV
174-
"424" to getEnum("FAILED_DEPENDENCY"), // WebDAV
175-
"425" to getEnum("TOO_EARLY"),
176-
"426" to getEnum("UPGRADE_REQUIRED"),
177-
"428" to getEnum("PRECONDITION_REQUIRED"),
178-
"429" to getEnum("TOO_MANY_REQUESTS"),
179-
"431" to getEnum("REQUEST_HEADER_FIELDS_TOO_LARGE"),
180-
"451" to getEnum("UNAVAILABLE_FOR_LEGAL_REASONS"),
181-
182-
"500" to getEnum("INTERNAL_SERVER_ERROR"),
183-
"501" to getEnum("NOT_IMPLEMENTED"),
184-
"502" to getEnum("BAD_GATEWAY"),
185-
"503" to getEnum("SERVICE_UNAVAILABLE"),
186-
"504" to getEnum("GATEWAY_TIMEOUT"),
187-
"505" to getEnum("HTTP_VERSION_NOT_SUPPORTED"),
188-
"506" to getEnum("VARIANT_ALSO_NEGOTIATES"),
189-
"507" to getEnum("INSUFFICIENT_STORAGE"), // WebDAV
190-
"508" to getEnum("LOOP_DETECTED"), // WebDAV
191-
"509" to getEnum("BANDWIDTH_LIMIT_EXCEEDED"),
192-
"510" to getEnum("NOT_EXTENDED"),
193-
"511" to getEnum("NETWORK_AUTHENTICATION_REQUIRED")
194-
)
195-
196-
private const val HTTP_STATUS_ENUM = "org.springframework.http.HttpStatus"
197-
198-
private fun getEnum(name: String): String {
199-
return "HttpStatus.${name}"
200-
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2026 https://github.com/openapi-processor/openapi-processor-spring
3+
* PDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.openapiprocessor.spring.processor
7+
8+
import io.openapiprocessor.core.converter.mapping.SimpleParameterValue
9+
import io.openapiprocessor.core.framework.FrameworkAnnotations
10+
import io.openapiprocessor.core.model.Annotation
11+
import io.openapiprocessor.core.model.EndpointResponseStatus
12+
import io.openapiprocessor.core.model.RequestBody
13+
import io.openapiprocessor.core.model.parameters.CookieParameter
14+
import io.openapiprocessor.core.model.parameters.HeaderParameter
15+
import io.openapiprocessor.core.model.parameters.Parameter
16+
import io.openapiprocessor.core.model.parameters.PathParameter
17+
import io.openapiprocessor.core.openapi.HttpMethod
18+
import io.openapiprocessor.spring.model.parameters.MultipartParameter
19+
import io.openapiprocessor.spring.model.parameters.QueryParameter
20+
import org.slf4j.Logger
21+
import org.slf4j.LoggerFactory
22+
23+
/**
24+
* provides Spring exchange annotation details.
25+
*/
26+
class SpringFrameworkExchange: FrameworkAnnotations {
27+
private val log: Logger = LoggerFactory.getLogger(this.javaClass.name)
28+
29+
override fun getAnnotation(httpMethod: HttpMethod): Annotation {
30+
if(EXCHANGE_ANNOTATIONS.containsKey(httpMethod)) {
31+
return EXCHANGE_ANNOTATIONS.getValue(httpMethod)
32+
}
33+
34+
return Annotation(
35+
getExchangeAnnotationName("Http"),
36+
linkedMapOf("method" to SimpleParameterValue(""""${httpMethod.toString().uppercase()}"""")))
37+
}
38+
39+
override fun getAnnotation(parameter: Parameter): Annotation {
40+
return when(parameter) {
41+
is RequestBody -> getAnnotation("body")
42+
is PathParameter -> getAnnotation("path")
43+
is QueryParameter -> getAnnotation("query")
44+
is HeaderParameter -> getAnnotation("header")
45+
is CookieParameter -> getAnnotation("cookie")
46+
is MultipartParameter -> getMultipartAnnotation(parameter.contentType)
47+
else -> {
48+
log.error("unknown parameter type: ${parameter::class.java.name}")
49+
UNKNOWN_ANNOTATION
50+
}
51+
}
52+
}
53+
54+
override fun getAnnotation(status: EndpointResponseStatus): Annotation {
55+
val statusCode = HTTP_STATUS[status.statusCode]
56+
if (statusCode == null) {
57+
log.error("unknown http status code: ${status.statusCode}")
58+
return UNKNOWN_ANNOTATION
59+
}
60+
61+
return Annotation(
62+
"org.springframework.web.bind.annotation.ResponseStatus",
63+
linkedMapOf(
64+
"code" to SimpleParameterValue(statusCode, HTTP_STATUS_ENUM)))
65+
}
66+
67+
private fun getAnnotation(key: String): Annotation {
68+
return PARAMETER_ANNOTATIONS.getValue(key)
69+
}
70+
71+
private fun getMultipartAnnotation(contentType: String?): Annotation {
72+
return if (contentType != null) {
73+
PARAMETER_ANNOTATIONS.getValue("multipart-part")
74+
} else {
75+
PARAMETER_ANNOTATIONS.getValue("multipart-param")
76+
}
77+
}
78+
}
79+
80+
private const val EXCHANGE_ANNOTATION_PKG = "org.springframework.web.service.annotation"
81+
82+
private fun getExchangeAnnotationName(methodName: String): String =
83+
"$EXCHANGE_ANNOTATION_PKG.${methodName}Exchange"
84+
85+
private val EXCHANGE_ANNOTATIONS = hashMapOf(
86+
HttpMethod.DELETE to Annotation(getExchangeAnnotationName("Delete")),
87+
HttpMethod.GET to Annotation(getExchangeAnnotationName("Get")),
88+
HttpMethod.HEAD to Annotation(getExchangeAnnotationName("Http"),
89+
linkedMapOf("method" to SimpleParameterValue(""""HEAD""""))),
90+
HttpMethod.OPTIONS to Annotation(getExchangeAnnotationName("Http"),
91+
linkedMapOf("method" to SimpleParameterValue(""""OPTIONS""""))),
92+
HttpMethod.PATCH to Annotation(getExchangeAnnotationName("Patch")),
93+
HttpMethod.POST to Annotation(getExchangeAnnotationName("Post")),
94+
HttpMethod.PUT to Annotation(getExchangeAnnotationName("Put")),
95+
HttpMethod.TRACE to Annotation(getExchangeAnnotationName("Http"),
96+
linkedMapOf("method" to SimpleParameterValue(""""TRACE"""")))
97+
)
98+
99+
private val PARAMETER_ANNOTATIONS = hashMapOf(
100+
"query" to Annotation ("org.springframework.web.service.annotation.QueryParam"),
101+
"header" to Annotation ("org.springframework.web.service.annotation.Header"),
102+
"cookie" to Annotation ("org.springframework.web.service.annotation.Cookie"),
103+
"path" to Annotation ("org.springframework.web.service.annotation.PathVariable"),
104+
"multipart-param" to Annotation ("org.springframework.web.service.annotation.Part"),
105+
"multipart-part" to Annotation ("org.springframework.web.service.annotation.Part"),
106+
"body" to Annotation ("org.springframework.web.service.annotation.RequestBody")
107+
)
108+
109+
private val UNKNOWN_ANNOTATION = Annotation("Unknown")
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2026 https://github.com/openapi-processor/openapi-processor-spring
3+
* PDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.openapiprocessor.spring.processor
7+
8+
// To avoid a dependency on Spring, the map provides the http status enum names.
9+
// This may break if the enum name does not exist in the used Spring version.
10+
11+
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
12+
13+
val HTTP_STATUS = hashMapOf(
14+
"100" to getEnum("CONTINUE"),
15+
"101" to getEnum("SWITCHING_PROTOCOLS"),
16+
"102" to getEnum("PROCESSING"), // WebDAV
17+
"103" to getEnum("CHECKPOINT"),
18+
19+
"200" to getEnum("OK"),
20+
"201" to getEnum("CREATED"),
21+
"202" to getEnum("ACCEPTED"),
22+
"203" to getEnum("NON_AUTHORITATIVE_INFORMATION"),
23+
"204" to getEnum("NO_CONTENT"),
24+
"205" to getEnum("RESET_CONTENT"),
25+
"206" to getEnum("PARTIAL_CONTENT"),
26+
"207" to getEnum("MULTI_STATUS"), // WebDAV
27+
"208" to getEnum("ALREADY_REPORTED"), // WebDAV
28+
"226" to getEnum("IM_USED"),
29+
30+
"300" to getEnum("MULTIPLE_CHOICES"),
31+
"301" to getEnum("MOVED_PERMANENTLY"),
32+
"302" to getEnum("MOVED_TEMPORARILY"),
33+
//"302" to getEnum("FOUND"), // replaces MOVED_TEMPORARILY
34+
"303" to getEnum("SEE_OTHER"),
35+
"304" to getEnum("NOT_MODIFIED"),
36+
"305" to getEnum("USE_PROXY"),
37+
"307" to getEnum("TEMPORARY_REDIRECT"),
38+
"308" to getEnum("PERMANENT_REDIRECT"),
39+
40+
"400" to getEnum("BAD_REQUEST"),
41+
"401" to getEnum("UNAUTHORIZED"),
42+
"402" to getEnum("PAYMENT_REQUIRED"),
43+
"403" to getEnum("FORBIDDEN"),
44+
"404" to getEnum("NOT_FOUND"),
45+
"405" to getEnum("METHOD_NOT_ALLOWED"),
46+
"406" to getEnum("NOT_ACCEPTABLE"),
47+
"407" to getEnum("PROXY_AUTHENTICATION_REQUIRED"),
48+
"408" to getEnum("REQUEST_TIMEOUT"),
49+
"409" to getEnum("CONFLICT"),
50+
"410" to getEnum("GONE"),
51+
"411" to getEnum("LENGTH_REQUIRED"),
52+
"412" to getEnum("PRECONDITION_FAILED"),
53+
"413" to getEnum("REQUEST_ENTITY_TOO_LARGE"),
54+
//"413" to getEnum("PAYLOAD_TOO_LARGE"), // replaces REQUEST_ENTITY_TOO_LARGE
55+
"414" to getEnum("REQUEST_URI_TOO_LONG"),
56+
//"414" to getEnum("URI_TOO_LONG"), // replaces REQUEST_URI_TOO_LONG
57+
"415" to getEnum("UNSUPPORTED_MEDIA_TYPE"),
58+
"416" to getEnum("REQUESTED_RANGE_NOT_SATISFIABLE"),
59+
"417" to getEnum("EXPECTATION_FAILED"),
60+
"418" to getEnum("I_AM_A_TEAPOT"),
61+
"419" to getEnum("INSUFFICIENT_SPACE_ON_RESOURCE"), // WebDAV
62+
"420" to getEnum("METHOD_FAILURE"), // WebDAV
63+
"421" to getEnum("DESTINATION_LOCKED"), // WebDAV
64+
"422" to getEnum("UNPROCESSABLE_ENTITY"), // WebDAV
65+
"423" to getEnum("LOCKED"), // WebDAV
66+
"424" to getEnum("FAILED_DEPENDENCY"), // WebDAV
67+
"425" to getEnum("TOO_EARLY"),
68+
"426" to getEnum("UPGRADE_REQUIRED"),
69+
"428" to getEnum("PRECONDITION_REQUIRED"),
70+
"429" to getEnum("TOO_MANY_REQUESTS"),
71+
"431" to getEnum("REQUEST_HEADER_FIELDS_TOO_LARGE"),
72+
"451" to getEnum("UNAVAILABLE_FOR_LEGAL_REASONS"),
73+
74+
"500" to getEnum("INTERNAL_SERVER_ERROR"),
75+
"501" to getEnum("NOT_IMPLEMENTED"),
76+
"502" to getEnum("BAD_GATEWAY"),
77+
"503" to getEnum("SERVICE_UNAVAILABLE"),
78+
"504" to getEnum("GATEWAY_TIMEOUT"),
79+
"505" to getEnum("HTTP_VERSION_NOT_SUPPORTED"),
80+
"506" to getEnum("VARIANT_ALSO_NEGOTIATES"),
81+
"507" to getEnum("INSUFFICIENT_STORAGE"), // WebDAV
82+
"508" to getEnum("LOOP_DETECTED"), // WebDAV
83+
"509" to getEnum("BANDWIDTH_LIMIT_EXCEEDED"),
84+
"510" to getEnum("NOT_EXTENDED"),
85+
"511" to getEnum("NETWORK_AUTHENTICATION_REQUIRED")
86+
)
87+
88+
const val HTTP_STATUS_ENUM = "org.springframework.http.HttpStatus"
89+
90+
fun getEnum(name: String): String {
91+
return "HttpStatus.${name}"
92+
}

src/main/kotlin/io/openapiprocessor/spring/processor/SpringProcessor.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ class SpringProcessor : OpenApiProcessorTest {
4040
}
4141

4242
val framework = SpringFramework()
43-
val annotations = SpringFrameworkAnnotations()
43+
44+
val annotations = when (processorOptions["annotations"]?.toString()) {
45+
"service-client" -> SpringFrameworkExchange()
46+
else -> SpringFrameworkAnnotations()
47+
}
4448

4549
val options = convertOptions(processorOptions)
4650
val identifier = JavaIdentifier(IdentifierOptions(
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2026 https://github.com/openapi-processor/openapi-processor-spring
3+
* PDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.openapiprocessor.spring.writer.java
7+
8+
import io.openapiprocessor.core.framework.FrameworkAnnotations
9+
import io.openapiprocessor.core.model.Endpoint
10+
import io.openapiprocessor.core.model.EndpointResponse
11+
import io.openapiprocessor.core.writer.java.MappingAnnotationFactory as CoreMappingAnnotationFactory
12+
13+
/**
14+
* spring exchange annotation factory.
15+
* org.springframework.web.service.annotation.HttpExchange was designed to be neutral to client vs server use.
16+
* The contentType attribute is for the request body, while the accept attribute is for the server response.
17+
*
18+
* | attribute | client side (request) | server side (controller, response) |
19+
* |-----------|-----------------------|------------------------------------|
20+
* |contentType| sends header | validates header |
21+
* |accept | sends header | sets contentType (on response) |
22+
*/
23+
class ExchangeAnnotationFactory(private val annotations: FrameworkAnnotations): CoreMappingAnnotationFactory {
24+
25+
override fun create(endpoint: Endpoint, endpointResponse: EndpointResponse): List<String> {
26+
return listOf(createAnnotation(endpoint, endpointResponse))
27+
}
28+
29+
private fun createAnnotation(endpoint: Endpoint, endpointResponse: EndpointResponse): String {
30+
val annotation = annotations.getAnnotation(endpoint.method)
31+
32+
var mapping = annotation.annotationName
33+
mapping += "("
34+
mapping += "url = " + quote(endpoint.path)
35+
36+
// todo warn if size is larger than 1?
37+
val consumes = endpoint.getConsumesContentTypes()
38+
if (consumes.isNotEmpty()) {
39+
mapping += ", "
40+
mapping += "contentType = "
41+
mapping += quote(consumes.first())
42+
}
43+
44+
val produces = endpointResponse.contentTypes
45+
if (produces.isNotEmpty()) {
46+
mapping += ", "
47+
mapping += "accept = {"
48+
49+
mapping += produces.joinToString(", ") {
50+
quote(it)
51+
}
52+
53+
mapping += "}"
54+
}
55+
56+
annotation.parameters.forEach {
57+
mapping += ", "
58+
mapping += "${it.key} = ${it.value.value}"
59+
}
60+
61+
mapping += ")"
62+
return mapping
63+
}
64+
65+
private fun quote(content: String): String {
66+
return '"' + content + '"'
67+
}
68+
}

src/main/kotlin/io/openapiprocessor/spring/writer/java/MappingAnnotationFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
package io.openapiprocessor.spring.writer.java
77

8+
import io.openapiprocessor.core.framework.FrameworkAnnotations
89
import io.openapiprocessor.core.model.Endpoint
910
import io.openapiprocessor.core.model.EndpointResponse
10-
import io.openapiprocessor.spring.processor.SpringFrameworkAnnotations
1111
import io.openapiprocessor.core.writer.java.MappingAnnotationFactory as CoreMappingAnnotationFactory
1212

1313
/**
1414
* spring mapping annotation factory
1515
*/
16-
class MappingAnnotationFactory(private val annotations: SpringFrameworkAnnotations): CoreMappingAnnotationFactory {
16+
class MappingAnnotationFactory(private val annotations: FrameworkAnnotations): CoreMappingAnnotationFactory {
1717

1818
override fun create(endpoint: Endpoint, endpointResponse: EndpointResponse): List<String> {
1919
return listOf(createAnnotation(endpoint, endpointResponse))

0 commit comments

Comments
 (0)