diff --git a/pom.xml b/pom.xml index a82f898689..cfd70b4ffd 100644 --- a/pom.xml +++ b/pom.xml @@ -703,6 +703,7 @@ osgi spring-katharsis logging-modules + spring-boot-documentation spring-boot-modules apache-httpclient apache-httpclient4 @@ -974,6 +975,7 @@ osgi spring-katharsis logging-modules + spring-boot-documentation spring-boot-modules apache-httpclient apache-httpclient4 diff --git a/spring-boot-documentation/pom.xml b/spring-boot-documentation/pom.xml new file mode 100644 index 0000000000..d718f33a99 --- /dev/null +++ b/spring-boot-documentation/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + com.baeldung.spring-boot-documentation + spring-boot-documentation + 1.0.0-SNAPSHOT + spring-boot-documentation + pom + + + com.baeldung + parent-boot-3 + 0.0.1-SNAPSHOT + ../parent-boot-3 + + + + springwolf + + + + + + org.junit + junit-bom + ${junit-jupiter.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + 3.3.2 + + + diff --git a/spring-boot-documentation/springwolf/docker-compose.yml b/spring-boot-documentation/springwolf/docker-compose.yml new file mode 100644 index 0000000000..214e2d2ace --- /dev/null +++ b/spring-boot-documentation/springwolf/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' +services: + zookeeper: + image: confluentinc/cp-zookeeper:latest + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: + + kafka: + image: confluentinc/cp-kafka:latest + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_HOST://kafka:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + + + akhq: + image: tchiotludo/akhq + restart: unless-stopped + environment: + AKHQ_CONFIGURATION: | + akhq: + connections: + docker-kafka-server: + properties: + bootstrap.servers: "kafka:29092" + + ports: + - "9090:8080" + links: + - kafka diff --git a/spring-boot-documentation/springwolf/pom.xml b/spring-boot-documentation/springwolf/pom.xml new file mode 100644 index 0000000000..4bd9f24065 --- /dev/null +++ b/spring-boot-documentation/springwolf/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + springwolf + 0.0.1-SNAPSHOT + springwolf + Documentation Spring Event Driven API Using AsyncAPI and Springwolf + + + com.baeldung.spring-boot-documentation + spring-boot-documentation + 1.0.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.kafka + spring-kafka + + + io.swagger.core.v3 + swagger-core-jakarta + ${swagger-core.version} + + + io.github.springwolf + springwolf-kafka + ${springwolf-kafka.version} + + + io.github.springwolf + springwolf-ui + ${springwolf-ui.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.springframework.kafka + spring-kafka-test + test + + + org.testcontainers + junit-jupiter + ${testcontainers-kafka.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.baeldung.boot.documentation.springwolf.SpringwolfApplication + + + + + + + 2.2.11 + 0.12.1 + 0.8.0 + 1.18.3 + + + diff --git a/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/SpringwolfApplication.java b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/SpringwolfApplication.java new file mode 100644 index 0000000000..1eed112a40 --- /dev/null +++ b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/SpringwolfApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.boot.documentation.springwolf; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringwolfApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringwolfApplication.class, args); + } +} diff --git a/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/adapter/incoming/IncomingConsumer.java b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/adapter/incoming/IncomingConsumer.java new file mode 100644 index 0000000000..ae201dbf30 --- /dev/null +++ b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/adapter/incoming/IncomingConsumer.java @@ -0,0 +1,48 @@ +package com.baeldung.boot.documentation.springwolf.adapter.incoming; + +import com.baeldung.boot.documentation.springwolf.dto.IncomingPayloadDto; +import com.baeldung.boot.documentation.springwolf.service.ProcessorService; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncListener; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.KafkaAsyncOperationBinding; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import static org.springframework.kafka.support.mapping.AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME; + +@AllArgsConstructor +@Component +@Slf4j +public class IncomingConsumer { + + private static final String TOPIC_NAME = "incoming-topic"; + + private final ProcessorService processorService; + + @KafkaListener(topics = TOPIC_NAME) + @AsyncListener(operation = @AsyncOperation( + channelName = TOPIC_NAME, + description = "More details for the incoming topic", + headers = @AsyncOperation.Headers( + schemaName = "SpringKafkaDefaultHeadersIncomingPayloadDto", + values = { + // this header is generated by Spring by default + @AsyncOperation.Headers.Header( + name = DEFAULT_CLASSID_FIELD_NAME, + description = "Spring Type Id Header", + value = "com.baeldung.boot.documentation.springwolf.dto.IncomingPayloadDto" + ), + } + ) + ) + ) + @KafkaAsyncOperationBinding + public void consume(IncomingPayloadDto payload) { + log.info("Received new message: {}", payload.toString()); + + processorService.doHandle(payload); + } + +} diff --git a/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/adapter/outgoing/OutgoingProducer.java b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/adapter/outgoing/OutgoingProducer.java new file mode 100644 index 0000000000..5630cd9a04 --- /dev/null +++ b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/adapter/outgoing/OutgoingProducer.java @@ -0,0 +1,47 @@ +package com.baeldung.boot.documentation.springwolf.adapter.outgoing; + +import com.baeldung.boot.documentation.springwolf.dto.OutgoingPayloadDto; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.KafkaAsyncOperationBinding; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +import static org.springframework.kafka.support.mapping.AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME; + +@AllArgsConstructor +@Component +@Slf4j +public class OutgoingProducer { + + private static final String TOPIC_NAME = "outgoing-topic"; + + private final KafkaTemplate kafkaTemplate; + + @AsyncPublisher( + operation = @AsyncOperation( + channelName = TOPIC_NAME, + description = "More details for the outgoing topic", + headers = @AsyncOperation.Headers( + schemaName = "SpringKafkaDefaultHeadersOutgoingPayloadDto", + values = { + // this header is generated by Spring by default + @AsyncOperation.Headers.Header( + name = DEFAULT_CLASSID_FIELD_NAME, + description = "Spring Type Id Header", + value = "com.baeldung.boot.documentation.springwolf.dto.OutgoingPayloadDto" + ), + } + ) + ) + ) + @KafkaAsyncOperationBinding + public void publish(OutgoingPayloadDto payload) { + log.info("Publishing new message: {}", payload.toString()); + + kafkaTemplate.send(TOPIC_NAME, payload); + } + +} diff --git a/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/dto/IncomingPayloadDto.java b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/dto/IncomingPayloadDto.java new file mode 100644 index 0000000000..a547d55db7 --- /dev/null +++ b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/dto/IncomingPayloadDto.java @@ -0,0 +1,25 @@ +package com.baeldung.boot.documentation.springwolf.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "Incoming payload model") +public class IncomingPayloadDto { + @Schema(description = "Some string field", example = "some string value", requiredMode = REQUIRED) + private String someString; + + @Schema(description = "Some long field", example = "5", requiredMode = NOT_REQUIRED) + private long someLong; + + @Schema(description = "Some enum field", example = "FOO2", requiredMode = REQUIRED) + private IncomingPayloadEnum someEnum; + + public enum IncomingPayloadEnum { + FOO1, FOO2, FOO3 + } + +} diff --git a/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/dto/OutgoingPayloadDto.java b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/dto/OutgoingPayloadDto.java new file mode 100644 index 0000000000..2fb1ab1647 --- /dev/null +++ b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/dto/OutgoingPayloadDto.java @@ -0,0 +1,20 @@ +package com.baeldung.boot.documentation.springwolf.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Builder +@Schema(description = "Outgoing payload model") +public class OutgoingPayloadDto { + + @Schema(description = "Foo field", example = "bar", requiredMode = NOT_REQUIRED) + private String foo; + + @Schema(description = "IncomingPayload field", requiredMode = REQUIRED) + private IncomingPayloadDto incomingWrapped; +} diff --git a/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/service/ProcessorService.java b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/service/ProcessorService.java new file mode 100644 index 0000000000..980f978f4e --- /dev/null +++ b/spring-boot-documentation/springwolf/src/main/java/com/baeldung/boot/documentation/springwolf/service/ProcessorService.java @@ -0,0 +1,23 @@ +package com.baeldung.boot.documentation.springwolf.service; + +import com.baeldung.boot.documentation.springwolf.adapter.outgoing.OutgoingProducer; +import com.baeldung.boot.documentation.springwolf.dto.OutgoingPayloadDto; +import com.baeldung.boot.documentation.springwolf.dto.IncomingPayloadDto; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class ProcessorService { + + private final OutgoingProducer outgoingProducer; + + public void doHandle(IncomingPayloadDto payload) { + OutgoingPayloadDto message = OutgoingPayloadDto.builder() + .foo("Foo message") + .incomingWrapped(payload) + .build(); + + outgoingProducer.publish(message); + } +} diff --git a/spring-boot-documentation/springwolf/src/main/resources/application.properties b/spring-boot-documentation/springwolf/src/main/resources/application.properties new file mode 100644 index 0000000000..74cb93499e --- /dev/null +++ b/spring-boot-documentation/springwolf/src/main/resources/application.properties @@ -0,0 +1,30 @@ +######### +# Spring Configuration +spring.application.name=Baeldung Tutorial Springwolf Application + +######### +# Spring Kafka Configuration +spring.kafka.bootstrap-servers=localhost:9092 +spring.kafka.consumer.group-id=baeldung-kafka-group-id +spring.kafka.consumer.properties.spring.json.trusted.packages=com.baeldung.boot.documentation.springwolf.* +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer + +######### +# Springwolf Configuration +springwolf.docket.base-package=com.baeldung.boot.documentation.springwolf.adapter +springwolf.docket.info.title=${spring.application.name} +springwolf.docket.info.version=1.0.0 +springwolf.docket.info.description=Baeldung Tutorial Application to Demonstrate AsyncAPI Documentation using Springwolf + +# Springwolf Kafka Configuration +springwolf.docket.servers.kafka.protocol=kafka +springwolf.docket.servers.kafka.url=localhost:9092 + +springwolf.plugin.kafka.publishing.enabled=true +springwolf.plugin.kafka.publishing.producer.bootstrap-servers=localhost:9092 +springwolf.plugin.kafka.publishing.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +springwolf.plugin.kafka.publishing.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer +springwolf.plugin.kafka.publishing.producer.properties.spring.json.add.type.headers=false diff --git a/spring-boot-documentation/springwolf/src/test/java/com/baeldung/boot/documentation/springwolf/ApiIntegrationTest.java b/spring-boot-documentation/springwolf/src/test/java/com/baeldung/boot/documentation/springwolf/ApiIntegrationTest.java new file mode 100644 index 0000000000..ac237c0da9 --- /dev/null +++ b/spring-boot-documentation/springwolf/src/test/java/com/baeldung/boot/documentation/springwolf/ApiIntegrationTest.java @@ -0,0 +1,41 @@ +package com.baeldung.boot.documentation.springwolf; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.test.annotation.DirtiesContext; +import org.testcontainers.shaded.org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@SpringBootTest(classes = {SpringwolfApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EmbeddedKafka( + partitions = 1, brokerProperties = { + "listeners=PLAINTEXT://localhost:9092", + "port=9092", +}) +@DirtiesContext +public class ApiIntegrationTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void asyncApiResourceArtifactTest() throws JSONException, IOException { + // given + InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); + String expected = IOUtils.toString(s, StandardCharsets.UTF_8); + + String url = "/springwolf/docs"; + String actual = restTemplate.getForObject(url, String.class); + + JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT); + } +} diff --git a/spring-boot-documentation/springwolf/src/test/resources/asyncapi.json b/spring-boot-documentation/springwolf/src/test/resources/asyncapi.json new file mode 100644 index 0000000000..0198733c1c --- /dev/null +++ b/spring-boot-documentation/springwolf/src/test/resources/asyncapi.json @@ -0,0 +1,168 @@ +{ + "asyncapi": "2.6.0", + "info": { + "title": "Baeldung Tutorial Springwolf Application", + "version": "1.0.0", + "description": "Baeldung Tutorial Application to Demonstrate AsyncAPI Documentation using Springwolf" + }, + "defaultContentType": "application/json", + "servers": { + "kafka": { + "url": "localhost:9092", + "protocol": "kafka" + } + }, + "channels": { + "incoming-topic": { + "publish": { + "operationId": "incoming-topic_publish", + "description": "More details for the incoming topic", + "bindings": { + "kafka": { } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "com.baeldung.boot.documentation.springwolf.dto.IncomingPayloadDto", + "title": "IncomingPayloadDto", + "description": "Incoming payload model", + "payload": { + "$ref": "#/components/schemas/IncomingPayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/SpringKafkaDefaultHeadersIncomingPayloadDto" + }, + "bindings": { + "kafka": { } + } + } + } + }, + "outgoing-topic": { + "subscribe": { + "operationId": "outgoing-topic_subscribe", + "description": "More details for the outgoing topic", + "bindings": { + "kafka": { } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "com.baeldung.boot.documentation.springwolf.dto.OutgoingPayloadDto", + "title": "OutgoingPayloadDto", + "description": "Outgoing payload model", + "payload": { + "$ref": "#/components/schemas/OutgoingPayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/SpringKafkaDefaultHeadersOutgoingPayloadDto" + }, + "bindings": { + "kafka": { } + } + } + } + } + }, + "components": { + "schemas": { + "HeadersNotDocumented": { + "type": "object", + "properties": { }, + "example": { } + }, + "IncomingPayloadDto": { + "required": [ + "someEnum", + "someString" + ], + "type": "object", + "properties": { + "someEnum": { + "type": "string", + "description": "Some enum field", + "example": "FOO2", + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ] + }, + "someLong": { + "type": "integer", + "description": "Some long field", + "format": "int64", + "example": 5 + }, + "someString": { + "type": "string", + "description": "Some string field", + "example": "some string value" + } + }, + "description": "Incoming payload model", + "example": { + "someEnum": "FOO2", + "someLong": 5, + "someString": "some string value" + } + }, + "OutgoingPayloadDto": { + "required": [ + "incomingWrapped" + ], + "type": "object", + "properties": { + "foo": { + "type": "string", + "description": "Foo field", + "example": "bar" + }, + "incomingWrapped": { + "$ref": "#/components/schemas/IncomingPayloadDto" + } + }, + "description": "Outgoing payload model", + "example": { + "foo": "bar", + "incomingWrapped": { + "someEnum": "FOO2", + "someLong": 5, + "someString": "some string value" + } + } + }, + "SpringKafkaDefaultHeadersIncomingPayloadDto": { + "type": "object", + "properties": { + "__TypeId__": { + "type": "string", + "description": "Spring Type Id Header", + "example": "com.baeldung.boot.documentation.springwolf.dto.IncomingPayloadDto", + "enum": [ + "com.baeldung.boot.documentation.springwolf.dto.IncomingPayloadDto" + ] + } + }, + "example": { + "__TypeId__": "com.baeldung.boot.documentation.springwolf.dto.IncomingPayloadDto" + } + }, + "SpringKafkaDefaultHeadersOutgoingPayloadDto": { + "type": "object", + "properties": { + "__TypeId__": { + "type": "string", + "description": "Spring Type Id Header", + "example": "com.baeldung.boot.documentation.springwolf.dto.OutgoingPayloadDto", + "enum": [ + "com.baeldung.boot.documentation.springwolf.dto.OutgoingPayloadDto" + ] + } + }, + "example": { + "__TypeId__": "com.baeldung.boot.documentation.springwolf.dto.OutgoingPayloadDto" + } + } + } + }, + "tags": [ ] +}