From 1ef6093f6dd2f3085755f7f10fc5d2e4b2a07dfe Mon Sep 17 00:00:00 2001 From: Semen Date: Tue, 16 Jan 2024 00:46:50 +0200 Subject: [PATCH 1/5] add blank files to support WS --- README.md | 7 ++++ package.json | 3 +- partials/WebSocketConfig.java | 23 +++++++++++++ partials/WebSocketPublisher.java | 31 +++++++++++++++++ partials/WebSocketPublisherImpl.java | 34 +++++++++++++++++++ template/build.gradle | 4 +++ template/pom.xml | 12 +++++++ .../com/asyncapi/infrastructure/Config.java | 4 +++ .../asyncapi/service/PublisherService.java | 3 ++ .../service/PublisherServiceImpl.java | 3 ++ 10 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 partials/WebSocketConfig.java create mode 100644 partials/WebSocketPublisher.java create mode 100644 partials/WebSocketPublisherImpl.java diff --git a/README.md b/README.md index 18b2eba7f..55de0b943 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,13 @@ asyncapi generate fromTemplate @asyncapi/java-spring-template You can replace `` with local path or URL pointing to [any AsyncAPI document](https://raw.githubusercontent.com/asyncapi/java-spring-template/master/tests/mocks/kafka.yml). +#### Supported protocols + +- Kafka +- AMQP +- MQTT +- WebSocket + ### AsyncAPI definitions To have correctly generated code, your AsyncAPI file MUST define `operationId` for every operation. diff --git a/package.json b/package.json index d7ce49781..e684774c7 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "supportedProtocols": [ "kafka", "amqp", - "mqtt" + "mqtt", + "ws" ], "nonRenderableFiles": [ "**/*.jar" diff --git a/partials/WebSocketConfig.java b/partials/WebSocketConfig.java new file mode 100644 index 000000000..8de78dedb --- /dev/null +++ b/partials/WebSocketConfig.java @@ -0,0 +1,23 @@ +{% macro wsConfig(asyncapi, params) %} + +import org.springframework.context.annotation.Configuration; + +import javax.annotation.processing.Generated; + +@Generated(value="com.asyncapi.generator.template.spring", date="{{''|currentTime }}") +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic"); + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/chat"); + registry.addEndpoint("/chat").withSockJS(); + } +} \ No newline at end of file diff --git a/partials/WebSocketPublisher.java b/partials/WebSocketPublisher.java new file mode 100644 index 000000000..45ece13c0 --- /dev/null +++ b/partials/WebSocketPublisher.java @@ -0,0 +1,31 @@ +{% macro wsPublisher(asyncapi, params) %} + +{% for channelName, channel in asyncapi.channels() %} + {%- if channel.hasSubscribe() %} + {%- for message in channel.subscribe().messages() %} +import {{params['userJavaPackage']}}.model.{{message.payload().uid() | camelCase | upperFirst}}; + {%- endfor -%} + {% endif -%} +{% endfor %} +import javax.annotation.processing.Generated; + +@Generated(value="com.asyncapi.generator.template.spring", date="{{''|currentTime }}") +public interface PublisherService { + +{% for channelName, channel in asyncapi.channels() %} + {%- if channel.hasSubscribe() %} + {%- set hasParameters = channel.hasParameters() %} + {%- if channel.subscribe().hasMultipleMessages() %} + {%- set varName = "object" %} + {%- else %} + {%- set varName = channel.subscribe().message().payload().uid() | camelCase %} + {%- endif %} + {% if channel.description() or channel.subscribe().description() %}/**{% for line in channel.description() | splitByLines %} + * {{line | safe}}{% endfor %}{% for line in channel.subscribe().description() | splitByLines %} + * {{line | safe}}{% endfor %} + */{% endif %} + public void {{channel.subscribe().id() | camelCase}}(Integer key, {{varName | upperFirst}} {{varName}}{% if hasParameters %}{%for parameterName, parameter in channel.parameters() %}, {% if parameter.schema().type() === 'object'%}{{parameterName | camelCase | upperFirst}}{% else %}{{parameter.schema().type() | toJavaType(false)}}{% endif %} {{parameterName | camelCase}}{% endfor %}{% endif %}); + {%- endif %} +{%- endfor %} +} +{% endmacro %} \ No newline at end of file diff --git a/partials/WebSocketPublisherImpl.java b/partials/WebSocketPublisherImpl.java new file mode 100644 index 000000000..a197af8c1 --- /dev/null +++ b/partials/WebSocketPublisherImpl.java @@ -0,0 +1,34 @@ +{% macro wsPublisherImpl(asyncapi, params) %} + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +{% for channelName, channel in asyncapi.channels() %} + {%- if channel.hasSubscribe() %} + {%- for message in channel.subscribe().messages() %} +import {{params['userJavaPackage']}}.model.{{message.payload().uid() | camelCase | upperFirst}}; + {%- endfor -%} + {% endif -%} +{% endfor %} +import javax.annotation.processing.Generated; + +@Generated(value="com.asyncapi.generator.template.spring", date="{{''|currentTime }}") +@Service +public class PublisherServiceImpl implements PublisherService { +{% for channelName, channel in asyncapi.channels() %} + {%- if channel.hasSubscribe() %} + {%- set hasParameters = channel.hasParameters() %} + {%- set methodName = channel.subscribe().id() | camelCase %} + {%- if channel.subscribe().hasMultipleMessages() %} + {%- set varName = "object" %} + {%- else %} + {%- set varName = channel.subscribe().message().payload().uid() | camelCase %} + {%- endif %} + {% if channel.description() or channel.subscribe().description() %}/**{% for line in channel.description() | splitByLines %} + * {{line | safe}}{% endfor %}{% for line in channel.subscribe().description() | splitByLines %} + * {{line | safe}}{% endfor %} + */{% endif %} + public void {{methodName}}(Integer key, {{varName | upperFirst}} {{varName}}{% if hasParameters %}{%for parameterName, parameter in channel.parameters() %}, {% if parameter.schema().type() === 'object'%}{{parameterName | camelCase | upperFirst}}{% else %}{{parameter.schema().type() | toJavaType(false)}}{% endif %} {{parameterName | camelCase}}{% endfor %}{% endif %}) { + + } +} +{% endmacro %} \ No newline at end of file diff --git a/template/build.gradle b/template/build.gradle index 8df60c13c..1cfd1ce7f 100644 --- a/template/build.gradle +++ b/template/build.gradle @@ -19,6 +19,10 @@ dependencies { {%- if asyncapi | isProtocol('mqtt') %} implementation('org.springframework.integration:spring-integration-mqtt') {% endif -%} + {%- if asyncapi | isProtocol('ws') %} + implementation('org.springframework:spring-websocket') + implementation('org.springframework:spring-messaging') + {% endif -%} {%- if asyncapi | isProtocol('kafka') %} {%- if params.springBoot2 %} implementation('org.springframework.kafka:spring-kafka:2.9.12') diff --git a/template/pom.xml b/template/pom.xml index 2d56f9be4..a6e080dd0 100644 --- a/template/pom.xml +++ b/template/pom.xml @@ -29,6 +29,18 @@ compile {% endif -%} + {%- if asyncapi | isProtocol('ws') %} + + org.springframework + spring-websocket + compile + + + org.springframework + spring-messaging + compile + + {% endif -%} {%- if asyncapi | isProtocol('kafka') %} {%- if params.springBoot2 %} diff --git a/template/src/main/java/com/asyncapi/infrastructure/Config.java b/template/src/main/java/com/asyncapi/infrastructure/Config.java index 3f530fec8..194f887fe 100644 --- a/template/src/main/java/com/asyncapi/infrastructure/Config.java +++ b/template/src/main/java/com/asyncapi/infrastructure/Config.java @@ -3,6 +3,7 @@ {%- from "partials/AmqpConfig.java" import amqpConfig -%} {%- from "partials/MqttConfig.java" import mqttConfig -%} {%- from "partials/KafkaConfig.java" import kafkaConfig -%} +{%- from "partials/WebSocketConfig.java" import wsConfig -%} {%- if asyncapi | isProtocol('amqp') -%} {{- amqpConfig(asyncapi, params) -}} @@ -12,4 +13,7 @@ {%- endif -%} {%- if (asyncapi | isProtocol('kafka')) or (asyncapi | isProtocol('kafka-secure')) -%} {{- kafkaConfig(asyncapi, params) -}} +{%- endif -%} +{%- if asyncapi | isProtocol('ws') -%} +{{- wsConfig(asyncapi, params) -}} {%- endif -%} \ No newline at end of file diff --git a/template/src/main/java/com/asyncapi/service/PublisherService.java b/template/src/main/java/com/asyncapi/service/PublisherService.java index 0555fa59e..5f886cf2c 100644 --- a/template/src/main/java/com/asyncapi/service/PublisherService.java +++ b/template/src/main/java/com/asyncapi/service/PublisherService.java @@ -2,10 +2,13 @@ {%- from "partials/CommonPublisher.java" import commonPublisher -%} {%- from "partials/KafkaPublisher.java" import kafkaPublisher -%} {%- from "partials/AmqpPublisher.java" import amqpPublisher -%} +{%- from "partials/WebSocketPublisher.java" import wsPublisher -%} {%- if asyncapi | isProtocol('kafka') -%} {{- kafkaPublisher(asyncapi, params) -}} {%- elif asyncapi | isProtocol('amqp') -%} {{- amqpPublisher(asyncapi, params) -}} +{%- elif asyncapi | isProtocol('ws') -%} +{{- wsPublisher(asyncapi, params) -}} {%- else -%} {{- commonPublisher(asyncapi, params) -}} {%- endif -%} \ No newline at end of file diff --git a/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java b/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java index 065aef77b..18d5a1dda 100644 --- a/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java +++ b/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java @@ -2,10 +2,13 @@ {%- from "partials/CommonPublisherImpl.java" import commonPublisherImpl -%} {%- from "partials/KafkaPublisherImpl.java" import kafkaPublisherImpl -%} {%- from "partials/AmqpPublisherImpl.java" import amqpPublisherImpl -%} +{%- from "partials/WebSocketPublisherImpl.java" import amqpPublisherImpl -%} {%- if asyncapi | isProtocol('kafka') -%} {{- kafkaPublisherImpl(asyncapi, params) -}} {%- elif asyncapi | isProtocol('amqp') -%} {{- amqpPublisherImpl(asyncapi, params) -}} +{%- elif asyncapi | isProtocol('ws') -%} +{{- wsPublisherImpl(asyncapi, params) -}} {%- else -%} {{- commonPublisherImpl(asyncapi, params) -}} {%- endif -%} \ No newline at end of file From 22c679565ac627c2d68f2b3b94b3d5afa68170f0 Mon Sep 17 00:00:00 2001 From: Semen Date: Wed, 7 Feb 2024 01:47:30 +0200 Subject: [PATCH 2/5] add mock api for web socker --- tests/mocks/ws.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/mocks/ws.yml diff --git a/tests/mocks/ws.yml b/tests/mocks/ws.yml new file mode 100644 index 000000000..8519c5dce --- /dev/null +++ b/tests/mocks/ws.yml @@ -0,0 +1,45 @@ +asyncapi: '2.0.0' +info: + title: Chat Application + version: '1.0.0' + description: A simple chat application using WebSocket protocol +servers: + production: + url: 'ws://example.com/websocket' + protocol: ws + description: Endpoint address for connections +channels: + chat: + description: Channel for sending messages + subscribe: + operationId: sendMessage + summary: Send a message + message: + $ref: '#/components/messages/ChatMessage' + chatResponse: + description: Channel for receiving messages + publish: + operationId: receiveMessage + summary: Receive a message + message: + $ref: '#/components/messages/ChatMessage' +components: + messages: + ChatMessage: + summary: The message in a chat + payload: + $ref: "#/components/schemas/MessagePayload" + schemas: + MessagePayload: + type: object + properties: + username: + type: string + content: + type: string + sentAt: + $ref: "#/components/schemas/sentAt" + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. \ No newline at end of file From a7ca158feb6e49ada6c1d3c03d125a286803f15f Mon Sep 17 00:00:00 2001 From: Semen Date: Wed, 7 Feb 2024 01:47:57 +0200 Subject: [PATCH 3/5] fix generation --- partials/WebSocketPublisherImpl.java | 2 ++ .../main/java/com/asyncapi/service/PublisherServiceImpl.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/partials/WebSocketPublisherImpl.java b/partials/WebSocketPublisherImpl.java index a197af8c1..6777f3802 100644 --- a/partials/WebSocketPublisherImpl.java +++ b/partials/WebSocketPublisherImpl.java @@ -30,5 +30,7 @@ public class PublisherServiceImpl implements PublisherService { public void {{methodName}}(Integer key, {{varName | upperFirst}} {{varName}}{% if hasParameters %}{%for parameterName, parameter in channel.parameters() %}, {% if parameter.schema().type() === 'object'%}{{parameterName | camelCase | upperFirst}}{% else %}{{parameter.schema().type() | toJavaType(false)}}{% endif %} {{parameterName | camelCase}}{% endfor %}{% endif %}) { } + {%- endif %} +{% endfor %} } {% endmacro %} \ No newline at end of file diff --git a/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java b/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java index 18d5a1dda..a327dbb83 100644 --- a/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java +++ b/template/src/main/java/com/asyncapi/service/PublisherServiceImpl.java @@ -2,7 +2,7 @@ {%- from "partials/CommonPublisherImpl.java" import commonPublisherImpl -%} {%- from "partials/KafkaPublisherImpl.java" import kafkaPublisherImpl -%} {%- from "partials/AmqpPublisherImpl.java" import amqpPublisherImpl -%} -{%- from "partials/WebSocketPublisherImpl.java" import amqpPublisherImpl -%} +{%- from "partials/WebSocketPublisherImpl.java" import wsPublisherImpl -%} {%- if asyncapi | isProtocol('kafka') -%} {{- kafkaPublisherImpl(asyncapi, params) -}} {%- elif asyncapi | isProtocol('amqp') -%} From 1520fc39d72cc67c0d5127e55c672753006ae596 Mon Sep 17 00:00:00 2001 From: Semen Date: Wed, 7 Feb 2024 01:48:24 +0200 Subject: [PATCH 4/5] update config --- partials/WebSocketConfig.java | 34 ++++++++++++++++++--- template/src/main/resources/application.yml | 6 ++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/partials/WebSocketConfig.java b/partials/WebSocketConfig.java index 8de78dedb..e8615bd1b 100644 --- a/partials/WebSocketConfig.java +++ b/partials/WebSocketConfig.java @@ -1,13 +1,25 @@ {% macro wsConfig(asyncapi, params) %} +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import javax.annotation.processing.Generated; +import java.net.URI; +import java.net.URISyntaxException; @Generated(value="com.asyncapi.generator.template.spring", date="{{''|currentTime }}") @Configuration @EnableWebSocketMessageBroker -public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { +public class WebSocketConfig extends WebSocketMessageBrokerConfigurer { + + {% for serverName, server in asyncapi.servers() %} + @Value("${ws.server.{{serverName}}}") + private String {{serverName}}Url; + {% endfor %} @Override public void configureMessageBroker(MessageBrokerRegistry config) { @@ -17,7 +29,21 @@ public void configureMessageBroker(MessageBrokerRegistry config) { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/chat"); - registry.addEndpoint("/chat").withSockJS(); + {% for serverName, server in asyncapi.servers() %} + registry.addEndpoint(getPath({{serverName}}Url)); + registry.addEndpoint(getPath({{serverName}}Url)).withSockJS(); + {% endfor %} + } + + private String getPath(String serverURL) { + String path; + try {URI uri = null; + uri = new URI(serverURL); + path = uri.getPath(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + return path; } -} \ No newline at end of file +} +{% endmacro %} \ No newline at end of file diff --git a/template/src/main/resources/application.yml b/template/src/main/resources/application.yml index 8f9796a44..daf67c7a4 100644 --- a/template/src/main/resources/application.yml +++ b/template/src/main/resources/application.yml @@ -70,4 +70,10 @@ spring: poll-timeout: {{params.listenerPollTimeout}} concurrency: {{params.listenerConcurrency}} {% endif %} +{% endif %} + +{%- if asyncapi | isProtocol('ws') %} +ws: + server: {% for serverName, server in asyncapi.servers() %} + {{serverName}}: {% if server.variable('port') %}{{server.url() | replace('{port}', server.variable('port').defaultValue())}}{% else %}{{server.url()}}{% endif %}{% endfor %} {% endif %} \ No newline at end of file From 6723e8805db76ae4d26dbf2925089d6148ce29b9 Mon Sep 17 00:00:00 2001 From: Semen Date: Thu, 8 Feb 2024 00:05:24 +0200 Subject: [PATCH 5/5] add message handling --- .../java/com/asyncapi/service/MessageHandlerService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/template/src/main/java/com/asyncapi/service/MessageHandlerService.java b/template/src/main/java/com/asyncapi/service/MessageHandlerService.java index f22707f61..23a47d0ee 100644 --- a/template/src/main/java/com/asyncapi/service/MessageHandlerService.java +++ b/template/src/main/java/com/asyncapi/service/MessageHandlerService.java @@ -15,6 +15,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.messaging.Message; import org.springframework.stereotype.Service; +{%- if asyncapi | isProtocol('ws') and hasPublish %} +import org.springframework.messaging.handler.annotation.MessageMapping; +{%- endif %} {%- if asyncapi | isProtocol('kafka') and hasPublish %} import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.support.KafkaHeaders; @@ -115,6 +118,11 @@ public class MessageHandlerService { // parametrized listener } {%- endif %} + {% elif asyncapi | isProtocol('ws') %} + @MessageMapping("/{{channelName}}") + public void handle{{methodName | upperFirst}}({{typeName}} payload) { + LOGGER.info("Message received from {{- channelName -}} : " + payload); + } {%- else %} {%- if hasParameters %} @Value("${mqtt.topic.{{-methodName-}}}")