Skip to content

Commit

Permalink
Refactor: Remove publishing code duplication (#425)
Browse files Browse the repository at this point in the history
* refactor: remove publishing code duplication

* refactor: remove publishing code duplication

* test: fix test and dependencies after refactor

* test: fix test and dependencies after refactor
  • Loading branch information
timonback authored Nov 3, 2023
1 parent c8d333d commit 0bf21f4
Show file tree
Hide file tree
Showing 23 changed files with 245 additions and 285 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.stavshamir.springwolf.asyncapi.AsyncApiSerializerService;
import io.github.stavshamir.springwolf.asyncapi.AsyncApiService;
import io.github.stavshamir.springwolf.asyncapi.DefaultAsyncApiSerializerService;
import io.github.stavshamir.springwolf.asyncapi.controller.ActuatorAsyncApiController;
import io.github.stavshamir.springwolf.asyncapi.controller.AsyncApiController;
import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
Expand All @@ -27,6 +30,12 @@ public AsyncApiController asyncApiController(
return new AsyncApiController(asyncApiService, asyncApiSerializerService);
}

@Bean
@ConditionalOnMissingBean
public PublishingPayloadCreator publishingPayloadCreator(SchemasService schemasService, ObjectMapper objectMapper) {
return new PublishingPayloadCreator(schemasService, objectMapper);
}

@Bean
@ConditionalOnProperty(name = SPRINGWOLF_ENDPOINT_ACTUATOR_ENABLED, havingValue = "true")
@ConditionalOnMissingBean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.controller;

import io.github.stavshamir.springwolf.asyncapi.controller.dtos.MessageDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpStatus;
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.RequestParam;

/**
* Used in plugins with publishing enabled.
* Located in springwolf-core to allow sharing of code
*/
@RequiredArgsConstructor
@Slf4j
public abstract class PublishingBaseController implements InitializingBean {

private final PublishingPayloadCreator publishingPayloadCreator;

protected abstract boolean isEnabled();

protected abstract void publishMessage(String topic, MessageDto message, Object payload);

@PostMapping("/publish")
public ResponseEntity<String> publish(@RequestParam String topic, @RequestBody MessageDto message) {
if (!isEnabled()) {
String errorMessage = "Publishing using %s is not enabled - message will not be published"
.formatted(this.getClass().getSimpleName());
log.warn(errorMessage);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorMessage);
}

PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message);
if (result.payload() != null) {
publishMessage(topic, message, result.payload());
return ResponseEntity.ok().build();
}
return ResponseEntity.badRequest().body(result.errorMessage());
}

@Override
public void afterPropertiesSet() {
log.debug(
"Message publishing via %s is active.".formatted(this.getClass().getSimpleName()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.stavshamir.springwolf.asyncapi.controller.dtos.MessageDto;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import io.swagger.v3.oas.models.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.text.MessageFormat;

/**
* Used in plugins with publishing enabled.
* Located in springwolf-core to allow sharing of code
*/
@RequiredArgsConstructor
@Slf4j
public class PublishingPayloadCreator {

private final SchemasService schemasService;
private final ObjectMapper objectMapper;

public Result createPayloadObject(MessageDto message) {

String messagePayloadType = message.getPayloadType();
for (Schema<?> value : schemasService.getDefinitions().values()) {
String schemaPayloadType = value.getName();
// security: match against user input, but always use our controlled data from the DefaultSchemaService
if (schemaPayloadType.equals(messagePayloadType)) {
try {
Class<?> payloadClass = Class.forName(schemaPayloadType);
Object payload = objectMapper.readValue(message.getPayload(), payloadClass);
return new Result(payload, null);
} catch (ClassNotFoundException | JsonProcessingException ex) {
String errorMessage = MessageFormat.format(
"Unable to create payload {0} from data: {1}", schemaPayloadType, message.getPayload());
log.info(errorMessage, ex);
return new Result(null, errorMessage);
}
}
}

String errorMessage = MessageFormat.format(
"Specified payloadType {0} is not a registered springwolf schema.", messagePayloadType);
log.info(errorMessage);
return new Result(null, errorMessage);
}

public record Result(Object payload, String errorMessage) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import io.github.stavshamir.springwolf.asyncapi.AsyncApiService;
import io.github.stavshamir.springwolf.fixtures.MinimalTestContextConfiguration;
import io.github.stavshamir.springwolf.fixtures.ObjectMapperTestConfiguration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -21,6 +22,7 @@ public class SpringContextIntegrationTest {
@ContextConfiguration(
classes = {
SpringwolfAutoConfiguration.class,
ObjectMapperTestConfiguration.class,
CustomBeanAsyncApiDocketConfiguration.class, // user has defined an own AsyncApiDocket bean
})
class AsyncApiDocketTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@ContextConfiguration(classes = {SpringwolfAutoConfiguration.class})
@ContextConfiguration(classes = {SpringwolfAutoConfiguration.class, ObjectMapperTestConfiguration.class})
@EnableConfigurationProperties
@TestPropertySource(
properties = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.fixtures;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@TestConfiguration
public class ObjectMapperTestConfiguration {

@ConditionalOnMissingBean
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
6 changes: 0 additions & 6 deletions springwolf-plugins/springwolf-amqp-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ dependencies {
implementation "com.asyncapi:asyncapi-core:${asyncapiCoreVersion}"
implementation "org.slf4j:slf4j-api:${slf4jApiVersion}"

implementation "org.springframework:spring-beans"
implementation "org.springframework:spring-context"
implementation "org.springframework:spring-core"
implementation "org.springframework:spring-web"
Expand All @@ -21,11 +20,6 @@ dependencies {
implementation "org.springframework.amqp:spring-amqp"
implementation "org.springframework.amqp:spring-rabbit"

implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"

implementation "io.swagger.core.v3:swagger-models-jakarta:${swaggerVersion}"

compileOnly "com.google.code.findbugs:jsr305:${jsr305Version}"
permitUnusedDeclared "com.google.code.findbugs:jsr305:${jsr305Version}"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.amqp;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.stavshamir.springwolf.asyncapi.AsyncApiService;
import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator;
import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfAmqpController;
import io.github.stavshamir.springwolf.producer.SpringwolfAmqpProducer;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
Expand Down Expand Up @@ -37,7 +36,7 @@ public SpringwolfAmqpProducer springwolfAmqpProducer(
@Bean
@ConditionalOnMissingBean
public SpringwolfAmqpController springwolfAmqpController(
SchemasService schemasService, SpringwolfAmqpProducer springwolfAmqpProducer, ObjectMapper objectMapper) {
return new SpringwolfAmqpController(schemasService, springwolfAmqpProducer, objectMapper);
PublishingPayloadCreator publishingPayloadCreator, SpringwolfAmqpProducer springwolfAmqpProducer) {
return new SpringwolfAmqpController(publishingPayloadCreator, springwolfAmqpProducer);
}
}
Original file line number Diff line number Diff line change
@@ -1,80 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.stavshamir.springwolf.asyncapi.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.stavshamir.springwolf.asyncapi.controller.dtos.MessageDto;
import io.github.stavshamir.springwolf.producer.SpringwolfAmqpProducer;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import io.swagger.v3.oas.models.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpStatus;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.text.MessageFormat;

@Slf4j
@RestController
@RequestMapping("/springwolf/amqp")
@RequiredArgsConstructor
public class SpringwolfAmqpController implements InitializingBean {

private final SchemasService schemasService;
@Slf4j
public class SpringwolfAmqpController extends PublishingBaseController {

private final SpringwolfAmqpProducer producer;

private final ObjectMapper objectMapper;

@PostMapping("/publish")
public void publish(@RequestParam String topic, @RequestBody MessageDto message) {
if (!producer.isEnabled()) {
log.warn("AMQP producer is not enabled - message will not be published");
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "AMQP producer is not enabled");
}

boolean foundDefinition = false;
String messagePayloadType = message.getPayloadType();
for (Schema<?> value : schemasService.getDefinitions().values()) {
String schemaPayloadType = value.getName();
// security: match against user input, but always use our controlled data from the DefaultSchemaService
if (schemaPayloadType.equals(messagePayloadType)) {
publishMessage(topic, message, schemaPayloadType);

foundDefinition = true;
break;
}
}

if (!foundDefinition) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "Specified payloadType is not a registered springwolf schema.");
}
public SpringwolfAmqpController(
PublishingPayloadCreator publishingPayloadCreator, SpringwolfAmqpProducer producer) {
super(publishingPayloadCreator);
this.producer = producer;
}

private void publishMessage(String topic, MessageDto message, String schemaPayloadType) {
try {
Class<?> payloadClass = Class.forName(schemaPayloadType);
Object payload = objectMapper.readValue(message.getPayload(), payloadClass);

log.debug("Publishing to amqp queue {}: {}", topic, message.getPayload());
producer.send(topic, payload);
} catch (ClassNotFoundException | JsonProcessingException ex) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
MessageFormat.format(
"Unable to create payload {0} from data: {1}", schemaPayloadType, message.getPayload()));
}
@Override
protected boolean isEnabled() {
return producer.isEnabled();
}

@Override
public void afterPropertiesSet() {
log.debug("Message publishing via " + this.getClass().getSimpleName() + " is active.");
protected void publishMessage(String topic, MessageDto message, Object payload) {
log.debug("Publishing to amqp queue {}: {}", topic, message.getPayload());
producer.send(topic, payload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import io.github.stavshamir.springwolf.asyncapi.AsyncApiService;
import io.github.stavshamir.springwolf.asyncapi.amqp.SpringwolfAmqpAutoConfiguration;
import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator;
import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfAmqpController;
import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner;
import io.github.stavshamir.springwolf.producer.SpringwolfAmqpProducer;
Expand All @@ -25,7 +26,12 @@
public class SpringwolfAmqpProducerConfigurationIntegrationTest {

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {SpringwolfAmqpAutoConfiguration.class, ObjectMapperTestConfiguration.class})
@ContextConfiguration(
classes = {
SpringwolfAmqpAutoConfiguration.class,
PublishingPayloadCreator.class,
ObjectMapperTestConfiguration.class
})
@TestPropertySource(
properties = {
"springwolf.enabled=true",
Expand Down Expand Up @@ -60,7 +66,12 @@ void springwolfAmqpProducerShouldBePresentInSpringContext() {
}

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {SpringwolfAmqpAutoConfiguration.class, ObjectMapperTestConfiguration.class})
@ContextConfiguration(
classes = {
SpringwolfAmqpAutoConfiguration.class,
PublishingPayloadCreator.class,
ObjectMapperTestConfiguration.class
})
@TestPropertySource(
properties = {
"springwolf.enabled=true",
Expand Down
5 changes: 1 addition & 4 deletions springwolf-plugins/springwolf-kafka-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ dependencies {
implementation "org.apache.kafka:kafka-clients:${kafkaClientsVersion}"
implementation "org.slf4j:slf4j-api:${slf4jApiVersion}"

implementation "org.springframework:spring-beans"
implementation "org.springframework:spring-context"
implementation "org.springframework:spring-core"
implementation "org.springframework:spring-web"
Expand All @@ -24,9 +23,6 @@ dependencies {

implementation "org.springframework.kafka:spring-kafka"

implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"

compileOnly "com.google.code.findbugs:jsr305:${jsr305Version}"
permitUnusedDeclared "com.google.code.findbugs:jsr305:${jsr305Version}"

Expand All @@ -46,6 +42,7 @@ dependencies {
testImplementation "org.springframework.boot:spring-boot-test-autoconfigure"
testImplementation "org.springframework.boot:spring-boot-test"

testImplementation "org.springframework:spring-beans"
testImplementation "org.springframework:spring-test"

testImplementation "org.assertj:assertj-core:${assertjCoreVersion}"
Expand Down
Loading

0 comments on commit 0bf21f4

Please sign in to comment.