diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md
index f6c41195627e..0009d819112b 100644
--- a/docs/generators/java-camel.md
+++ b/docs/generators/java-camel.md
@@ -87,6 +87,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|scmDeveloperConnection|SCM developer connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git|
|scmUrl|SCM URL in generated pom.xml| |https://github.com/openapitools/openapi-generator|
|serializableModel|boolean - toggle "implements Serializable" for generated models| |false|
+|serverSentEvents|enable server sent events support| |false|
|singleContentTypes|Whether to select only one produces/consumes content-type by operation.| |false|
|skipDefaultInterface|Whether to skip generation of default implementations for java8 interfaces| |false|
|snapshotVersion|Uses a SNAPSHOT version.|
- **true**
- Use a SnapShot Version
- **false**
- Use a Release Version
|null|
diff --git a/docs/generators/spring.md b/docs/generators/spring.md
index ec4a499b39a9..94a12dcb11fe 100644
--- a/docs/generators/spring.md
+++ b/docs/generators/spring.md
@@ -80,6 +80,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|scmDeveloperConnection|SCM developer connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git|
|scmUrl|SCM URL in generated pom.xml| |https://github.com/openapitools/openapi-generator|
|serializableModel|boolean - toggle "implements Serializable" for generated models| |false|
+|serverSentEvents|enable server sent events support| |false|
|singleContentTypes|Whether to select only one produces/consumes content-type by operation.| |false|
|skipDefaultInterface|Whether to skip generation of default implementations for java8 interfaces| |false|
|snapshotVersion|Uses a SNAPSHOT version.|- **true**
- Use a SnapShot Version
- **false**
- Use a Release Version
|null|
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
index 9a613c3bb67b..8c755bb3645e 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
@@ -219,6 +219,7 @@ public SpringCodegen() {
cliOptions.add(CliOption.newBoolean(ASYNC, "use async Callable controllers", async));
cliOptions.add(CliOption.newBoolean(REACTIVE, "wrap responses in Mono/Flux Reactor types (spring-boot only)",
reactive));
+ cliOptions.add(CliOption.newBoolean(SSE, "enable server sent events support", sse));
cliOptions.add(new CliOption(RESPONSE_WRAPPER,
"wrap the responses in given type (Future, Callable, CompletableFuture,ListenableFuture, DeferredResult, RxObservable, RxSingle or fully qualified type)"));
cliOptions.add(CliOption.newBoolean(VIRTUAL_SERVICE,
@@ -406,9 +407,11 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(ASYNC, this::setAsync);
if (additionalProperties.containsKey(REACTIVE)) {
if (SPRING_CLOUD_LIBRARY.equals(library)) {
- throw new IllegalArgumentException("Currently, reactive option doesn't supported by Spring Cloud");
+ throw new IllegalArgumentException("Currently, reactive option isn't supported by Spring Cloud");
}
convertPropertyToBooleanAndWriteBack(REACTIVE, this::setReactive);
+ }
+ if (additionalProperties.containsKey(SSE)) {
convertPropertyToBooleanAndWriteBack(SSE, this::setSse);
}
@@ -1026,53 +1029,59 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
codegenOperation.imports.add("ApiIgnore");
}
if (sse) {
- var MEDIA_EVENT_STREAM = "text/event-stream";
- // inspecting used streaming media types
- /*
- expected definition:
- content:
- text/event-stream:
- schema:
- type: array
- format: event-stream
- items:
- type: or
- $ref:
- */
- Map> schemaTypes = operation.getResponses().entrySet().stream()
- .map(e -> Pair.of(e.getValue(), fromResponse(e.getKey(), e.getValue())))
- .filter(p -> p.getRight().is2xx) // consider only success
- .map(p -> p.getLeft().getContent().get(MEDIA_EVENT_STREAM))
- .map(MediaType::getSchema)
- .collect(Collectors.toList()).stream()
- .collect(Collectors.groupingBy(Schema::getType));
- if(schemaTypes.containsKey("array")) {
- // we have a match with SSE pattern
- // double check potential conflicting, multiple specs
- if(schemaTypes.keySet().size() > 1) {
- throw new RuntimeException("only 1 response media type supported, when SSE is detected");
- }
- // double check schema format
- List eventTypes = schemaTypes.get("array");
- if( eventTypes.stream().anyMatch(schema -> !"event-stream".equalsIgnoreCase(schema.getFormat()))) {
- throw new RuntimeException("schema format 'event-stream' is required, when SSE is detected");
- }
- // double check item types
- Set itemTypes = eventTypes.stream()
- .map(schema -> schema.getItems().getType() != null
- ? schema.getItems().getType()
- : schema.getItems().get$ref())
- .collect(Collectors.toSet());
- if(itemTypes.size() > 1) {
- throw new RuntimeException("only single item type is supported, when SSE is detected");
- }
- codegenOperation.vendorExtensions.put("x-sse", true);
- } // Not an SSE compliant definition
+ handleSseConfiguration(operation, codegenOperation);
}
+ } else if (sse) {
+ handleSseConfiguration(operation, codegenOperation);
}
+
return codegenOperation;
}
+ // inspecting used streaming media types
+ /*
+ expected definition:
+ content:
+ text/event-stream:
+ schema:
+ type: array
+ format: event-stream
+ items:
+ type: or
+ $ref:
+ */
+ private void handleSseConfiguration(Operation operation, CodegenOperation codegenOperation) {
+ Map> schemaTypes = operation.getResponses().entrySet().stream()
+ .map(e -> Pair.of(e.getValue(), fromResponse(e.getKey(), e.getValue())))
+ .filter(p -> p.getRight().is2xx) // consider only success
+ .map(p -> p.getLeft().getContent().get("text/event-stream"))
+ .map(MediaType::getSchema)
+ .collect(Collectors.toList()).stream()
+ .collect(Collectors.groupingBy(Schema::getType));
+ if (schemaTypes.containsKey("array")) {
+ // we have a match with SSE pattern
+ // double check potential conflicting, multiple specs
+ if (schemaTypes.keySet().size() > 1) {
+ throw new RuntimeException("only 1 response media type supported, when SSE is detected");
+ }
+ // double check schema format
+ List eventTypes = schemaTypes.get("array");
+ if (eventTypes.stream().anyMatch(schema -> !"event-stream".equalsIgnoreCase(schema.getFormat()))) {
+ throw new RuntimeException("schema format 'event-stream' is required, when SSE is detected");
+ }
+ // double check item types
+ Set itemTypes = eventTypes.stream()
+ .map(schema -> schema.getItems().getType() != null
+ ? schema.getItems().getType()
+ : schema.getItems().get$ref())
+ .collect(Collectors.toSet());
+ if (itemTypes.size() > 1) {
+ throw new RuntimeException("only single item type is supported, when SSE is detected");
+ }
+ codegenOperation.vendorExtensions.put("x-sse", true);
+ }
+ }
+
private Set reformatProvideArgsParams(Operation operation) {
Set provideArgsClassSet = new HashSet<>();
Object argObj = operation.getExtensions().get("x-spring-provide-args");
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache
index ca566b626bd5..cfe1aa17151e 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache
@@ -55,6 +55,11 @@ import org.springframework.web.context.request.NativeWebRequest;
{{/reactive}}
{{/jdk8-no-delegate}}
import org.springframework.web.multipart.MultipartFile;
+{{^reactive}}
+{{#serverSentEvents}}
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+{{/serverSentEvents}}
+{{/reactive}}
{{#reactive}}
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
@@ -246,7 +251,9 @@ public interface {{classname}} {
{{#vendorExtensions.x-operation-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-operation-extra-annotation}}
- {{#vendorExtensions.x-sse}}@ResponseBody{{/vendorExtensions.x-sse}}
+ {{#reactive}}{{#vendorExtensions.x-sse}}
+ @ResponseBody
+ {{/vendorExtensions.x-sse}}{{/reactive}}
{{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{>responseType}} {{#delegate-method}}_{{/delegate-method}}{{operationId}}(
{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{^-last}},
{{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}},
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/apiDelegate.mustache
index 20ed2e4ac0d3..dc906d10ebe0 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/apiDelegate.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/apiDelegate.mustache
@@ -9,6 +9,11 @@ import org.springframework.http.ResponseEntity;
{{/useResponseEntity}}
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;
+{{^reactive}}
+{{#serverSentEvents}}
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+{{/serverSentEvents}}
+{{/reactive}}
{{#reactive}}
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/methodBody.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/methodBody.mustache
index bbbc66de3975..05b66b329a00 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/methodBody.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/methodBody.mustache
@@ -1,5 +1,6 @@
{{^reactive}}
{{#examples}}
+{{^vendorExtensions.x-sse}}
{{#-first}}
{{#async}}
return CompletableFuture.supplyAsync(()-> {
@@ -22,12 +23,27 @@ return CompletableFuture.supplyAsync(()-> {
}, Runnable::run);
{{/async}}
{{/-last}}
+{{/vendorExtensions.x-sse}}
+{{#vendorExtensions.x-sse}}
+{{#useResponseEntity}}return new SseEmitter<>();
+{{/useResponseEntity}}
+{{^useResponseEntity}}throw new IllegalArgumentException("Not implemented");
+{{/useResponseEntity}}
+{{/vendorExtensions.x-sse}}
{{/examples}}
{{^examples}}
+{{^vendorExtensions.x-sse}}
{{#useResponseEntity}}return {{#async}}CompletableFuture.completedFuture({{/async}}new ResponseEntity<>({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}){{#async}}){{/async}};
{{/useResponseEntity}}
{{^useResponseEntity}}throw new IllegalArgumentException("Not implemented");
{{/useResponseEntity}}
+{{/vendorExtensions.x-sse}}
+{{#vendorExtensions.x-sse}}
+{{#useResponseEntity}}return new SseEmitter<>();
+{{/useResponseEntity}}
+{{^useResponseEntity}}throw new IllegalArgumentException("Not implemented");
+{{/useResponseEntity}}
+{{/vendorExtensions.x-sse}}
{{/examples}}
{{/reactive}}
{{#reactive}}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/responseType.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/responseType.mustache
index a25da6310b56..8c0991f0a139 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/responseType.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/responseType.mustache
@@ -1 +1 @@
-{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}MonoreturnTypes}}{{#isArray}}>{{/isArray}}>>{{/useResponseEntity}}{{^useResponseEntity}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-sse}}
\ No newline at end of file
+{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}MonoreturnTypes}}{{#isArray}}>{{/isArray}}>>{{/useResponseEntity}}{{^useResponseEntity}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#reactive}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/reactive}}{{^reactive}}SseEmitter{{/reactive}}{{/vendorExtensions.x-sse}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java
index 0645b61eafb5..c5d3eadeb583 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java
@@ -4475,8 +4475,7 @@ public void multiLineTagDescription() throws IOException {
}
@Test
- public void testSSEOperationSupport() throws Exception {
-
+ public void testSSEOperationSupportReactive() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
@@ -4532,7 +4531,66 @@ public void testSSEOperationSupport() throws Exception {
.assertMethod("nonSSE", "ServerWebExchange")
.isNotNull()
.hasReturnType("Mono>")
- .bodyContainsLines("return result.then(Mono.empty());")
+ .bodyContainsLines("return result.then(Mono.empty());");
+ }
+
+ @Test
+ public void testSSEOperationSupportBlocking() throws Exception {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/sse.yaml");
+ final SpringCodegen codegen = new SpringCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(SSE, "true");
+ codegen.additionalProperties().put(INTERFACE_ONLY, "false");
+ codegen.additionalProperties().put(DELEGATE_PATTERN, "true");
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
+ generator.setGenerateMetadata(false);
+
+ Map files = generator.opts(input).generate().stream()
+ .collect(Collectors.toMap(File::getName, Function.identity()));
+
+ MapAssert.assertThatMap(files).isNotEmpty();
+ File api = files.get("PathApi.java");
+ File delegate = files.get("PathApiDelegate.java");
+
+ JavaFileAssert.assertThat(api)
+ .assertMethod("sseVariant1")
+ .isNotNull()
+ .hasReturnType("SseEmitter")
+ .toFileAssert()
+ .assertMethod("sseVariant2")
+ .isNotNull()
+ .hasReturnType("SseEmitter")
+ .toFileAssert()
+ .assertMethod("nonSSE")
+ .isNotNull()
+ .hasReturnType("ResponseEntity");
+
+ JavaFileAssert.assertThat(delegate)
+ .assertMethod("sseVariant1")
+ .isNotNull()
+ .hasReturnType("SseEmitter")
+ .bodyContainsLines("return new SseEmitter<>();")
+ .toFileAssert()
+ .assertMethod("sseVariant2")
+ .isNotNull()
+ .hasReturnType("SseEmitter")
+ .bodyContainsLines("return new SseEmitter<>();")
+ .toFileAssert()
+ .assertMethod("nonSSE")
+ .isNotNull()
+ .hasReturnType("ResponseEntity")
+ .bodyContainsLines("return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);")
;
}