skip to Main Content

I have a web service with a couple of endpoints, one of them is a PUT endpoint that receives an XML and adds some information to a Redis server.

My endpoint is mapped in a configuration class:

@Configuration 
public class RouterConfig {
    @Autowired
    private YAMLConfig yamlConfig;

    @Bean
    public RouterFunction<ServerResponse> getRoutes(final ServiceHandler serviceHandler) {
        return RouterFunctions.route()
            .GET(yamlConfig.getEnvPrefix() + STATUS_ENDPOINT, serviceHandler::getStatus)
            .GET(yamlConfig.getEnvPrefix() + STOCK_CAP_API + PRODUCT_CAPPING_ENDPOINT, serviceHandler::getStockCapForProducts)
            .PUT(yamlConfig.getEnvPrefix() + STOCK_CAP_API + ADD_PRODUCT_CAP_FOR_CUSTOMER, RequestPredicates.contentType(MediaType.APPLICATION_XML), serviceHandler::AddStockCap)
            .build();
    }
}

The handler mehtod for the PUT endpoint looks like this:

@NonNull
public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
    log.info("Adding new stock caps");
    serverRequest.bodyToMono(CustomerCap.class)
        .flatMap(stockCapService::addCustomerStockCap)
        .doOnEach(result -> System.out.println("Cap added - " + result));
    return ServerResponse.ok().build();
}

The bodyToMono is mapped to a class that represents the XML payload like the one below, and the attribute Customer inside the class is also mapped to a class in the same fashion.

@XmlRootElement(name="customerCap")
@XmlAccessorType(XmlAccessType.FIELD)
public class CustomerCap {
    @XmlElement
    private List<Customer> customer;

    public List<Customer> getCustomer() {
        return customer;
    }

    public void setCustomer(List<Customer> customer) {
        this.customer = customer;
    }
}

The implementation for the addCustomerStockCap is

@Override
public Mono<Boolean> addCustomerStockCap(CustomerCap customerCap) {
    log.info("Starting adding process");
    customerCap.getCustomer().forEach(customer -> {
        final var customerUID = customer.getCustomerNumber();
        customer.getCap().forEach(cap -> {
            redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), CAP_LIMIT, String.valueOf(cap.getCustomerCapLimit()));
            redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_FROM, String.valueOf(cap.getCapValidFrom()));
            redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_TO, String.valueOf(cap.getCapValidTo()));
            setExpirationOnHash(customerUID, cap);
        });
    });
    return Mono.just(Boolean.TRUE);
}

The method to handle the endpoint is being invoked properly and I can see the "Addin new stock caps" message on the log, but then the addCustomerStockCap is never invoked, with no exceptions or errors are being thrown, the method simply doesn’t execute.

Another test that I did was creating a controller instead of a functional endpoint. The controller method ended up like this

@PutMapping(STOCK_CAP_API + ADD_PRODUCT_CAP_FOR_CUSTOMER)
@ResponseStatus(HttpStatus.OK)
public void AddStockCap(@RequestBody CustomerCap customerCap) {
    log.info("Adding new stock caps");;
    stockCapService.addCustomerStockCap(customerCap);
}

When I did this I got some errors on the XML parse that I fixed only adding the annotation @NoArgsConstructor to the CustomerCap and other element classes. After this fix, the controller/endpoint worked as expected.

The problem still is that I need to make it work on the functional endpoint, we have several applications and we use functional endpoints as our standard so leaving only this endpoint like this wouldn’t make sense.

2

Answers


  1. Chosen as BEST ANSWER

    The only way that worked for me to make sure the method is invoked was to add its result as the response body.

    The problematic method ended up like this:

    @Override
    public Boolean addCustomerStockCap(CustomerCap customerCap) {
      log.info("Starting adding process");
      customerCap.getCustomer().forEach(customer -> {
          final var customerUID = customer.getCustomerNumber();
          customer.getCap().forEach(cap -> {
              redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), CAP_LIMIT, String.valueOf(cap.getCustomerCapLimit()));
              redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_FROM, String.valueOf(cap.getCapValidFrom()));
              redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_TO, String.valueOf(cap.getCapValidTo()));
              setExpirationOnHash(customerUID, cap);
          });
      });
      return Boolean.TRUE;
    }
    

    And the handler like this

    @NonNull
    public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
        log.info("Adding new stock caps");
        return ServerResponse.ok().body(serverRequest.bodyToFlux(CustomerCap.class).map(stockCapService::addCustomerStockCap), Boolean.class);
    }
    

    Not ideal as the goal was for this endpoint to only return a 200 code with no body, but it's a workaround until a found another way to invoke it.

    Also on spring documentation, for XML and JSON payload we should use Flux instead of Mono

    The following example extracts the body to a Flux (or a Flow in Kotlin), where Person objects are decoded from someserialized form, such as JSON or XML:

    https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-fn-handler-functions


  2. The issue is that your Mono is never subscribed and thus nothing is emitted and as a consequence stockCapService::addCustomerStockCap is never invoked. The following should work:

    @NonNull
    public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
        log.info("Adding new stock caps");
        serverRequest.bodyToMono(CustomerCap.class)
            .flatMap(stockCapService::addCustomerStockCap)
            .doOnEach(result -> System.out.println("Cap added - " + result))
            .subscribe();
        return ServerResponse.ok().build();
    }
    

    Or

    @NonNull
    public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
        log.info("Adding new stock caps");
        return serverRequest.bodyToMono(CustomerCap.class)
            .flatMap(stockCapService::addCustomerStockCap)
            .doOnEach(result -> System.out.println("Cap added - " + result))
            .flatMap(ServerResponse.ok().build());
    }
    

    Update 21/11/2021

    Try the following. You don’t really need addCustomerStockCap to return a Mono<Boolean>:

    @Override
    public Boolean addCustomerStockCap(CustomerCap customerCap) {
        log.info("Starting adding process");
        customerCap.getCustomer().forEach(customer -> {
            final var customerUID = customer.getCustomerNumber();
            customer.getCap().forEach(cap -> {
                redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), CAP_LIMIT, String.valueOf(cap.getCustomerCapLimit()));
                redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_FROM, String.valueOf(cap.getCapValidFrom()));
                redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_TO, String.valueOf(cap.getCapValidTo()));
                setExpirationOnHash(customerUID, cap);
            });
        });
        return Boolean.TRUE;
    }
    

    And now you can somewhat simplify your AddStockCap (flatMap turns a map):

    @NonNull
    public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
        log.info("Adding new stock caps");
        return serverRequest.bodyToMono(CustomerCap.class)
            .map(stockCapService::addCustomerStockCap)
            .flatMap(ServerResponse.ok().build());
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search