Broadleaf Microservices

Overriding Broadleaf Endpoints

Broadleaf exposes REST endpoints in each microservice to allow client interactions. Many of these endpoints permit basic CRUD operations for domain management by an admin. Others are intended to provide data for customer-facing, storefront pages.

In this tutorial we will look at how to extend the existing framework endpoints and how to handle some common gotchas.

Simple Endpoint Override Example

Broadleaf’s Spring Framework-Mappings Library

Broadleaf has introduced a library to enable user-defined, @RequestMapping annotated endpoints to override the default, framework-defined ones: Spring Framework-Mappings. Thus, in our microservices, you will find endpoints marked with @FrameworkMapping, @FrameworkGetMapping, etc. instead of @RequestMapping or @GetMapping. These annotations behave in the same way as the default Spring ones except that they have a lower priority when a request is resolved. Therefore, if you add @GetMapping("/products") to your own endpoint, it will take precedence over a framework endpoint with @FrameworkGetMapping("/products").

Example Broadleaf Endpoint

Next, let’s take a look at an example framework endpoint.

Product Endpoint Example
import org.broadleafcommerce.frameworkmapping.annotation.FrameworkGetMapping;
import org.broadleafcommerce.frameworkmapping.annotation.FrameworkMapping;
import org.broadleafcommerce.frameworkmapping.annotation.FrameworkRestController;
import org.springframework.web.bind.annotation.PathVariable;

import com.broadleafcommerce.catalog.domain.product.Product;
import com.broadleafcommerce.catalog.service.product.ProductService;
import com.broadleafcommerce.common.extension.data.DataRouteByExample;
import com.broadleafcommerce.data.tracking.core.context.ContextInfo;
import com.broadleafcommerce.data.tracking.core.context.ContextOperation;
import com.broadleafcommerce.data.tracking.core.policy.Policy;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@FrameworkRestController (1)
@FrameworkMapping("/products") (2)
@DataRouteByExample(Product.class) (3)
@RequiredArgsConstructor
public class ProductEndpoint {

    @Getter(AccessLevel.PROTECTED)
    private final ProductService<Product> productSvc;

    @FrameworkGetMapping("/{id}") (4)
    @Policy(permissionRoots = "PRODUCT") (5)
    public Product readProductById(@ContextOperation ContextInfo context, (6) (7)
          @PathVariable("id") String productId) {
        return productSvc.readByContextId(productId, context);
    }

}
  1. Equivalent to Spring’s @RestController

  2. Equivalent to @RequestMapping

  3. @DataRouteByExample identifies an example class whose package name is used to narrow requests to the appropriate data source, e.g., the one with the Product schema.

  4. Equivalent to @GetMapping

  5. Used to annotate a method that should be validated against one or more policies before the method is allowed to execute. For this endpoint, the system would validate that the caller has READ_PRODUCT permissions since it’s a read (GET) endpoint.

  6. @ContextOperation is used to initialize the ContextInfo argument—see the ContextInfoHandlerMethodArgumentResolver javadocs. The values are derived from the X-Context-Request header. This annotation takes a single value argument specifying the type of operation endpoint: CREATE, READ, UPDATE, DELETE, UNKNOWN. The default value is READ, but for other endpoints like for POST or PUT, you would specify @ContextOperation(ContextOperation.UPDATE)

  7. ContextInfo represents context information regarding the current API request such as the tenant, application, currency, etc.—see the ContextInfo javadocs.

Override Examples

With extending the base endpoint

You can simply extend the base controller and add the appropriate Spring request-mapping annotations.

Important
Remember to also component scan your override. It will automatically override the specified mappings.
Product Endpoint Extension
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.broadleafcommerce.catalog.domain.product.Product;
import com.broadleafcommerce.catalog.service.product.ProductService;
import com.broadleafcommerce.catalog.web.endpoint.ProductEndpoint;
import com.broadleafcommerce.data.tracking.core.context.ContextInfo;
import com.broadleafcommerce.data.tracking.core.context.ContextOperation;
import com.broadleafcommerce.data.tracking.core.policy.Policy;

@RestController
@RequestMapping("/products")
public class CustomProductEndpoint extends ProductEndpoint {

    public CustomProductEndpoint(ProductService<Product> productSvc) {
        super(productSvc);
    }

    @Override
    @GetMapping("/{id}")
    @Policy(permissionRoots = "PRODUCT")
    public Product readProductById(@ContextOperation ContextInfo context,
          @PathVariable("id") String productId) {
        return getProductSvc().readByContextId(productId, context);
    }

}

Without Extending the base endpoint

However, extending the base controller isn’t necessary to override the mappings. Whether you extend or just use the same request mappings will depend on whether you need to reference anything in the base class.

Important
Remember to add @DataRouteByExample to ensure the right data source can be determined.
Simple Product Endpoint Override
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.broadleafcommerce.common.extension.data.DataRouteByExample;
import com.broadleafcommerce.catalog.web.endpoint.ProductEndpoint;
import com.broadleafcommerce.data.tracking.core.context.ContextInfo;
import com.broadleafcommerce.data.tracking.core.context.ContextOperation;
import com.broadleafcommerce.data.tracking.core.policy.Policy;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/products")
@DataRouteByExample(Product.class)
@RequiredArgsConstructor
public class CustomProductEndpoint {

    private final CustomProductSvc<CustomProduct> productSvc;

    @GetMapping("/{id}")
    @Policy(permissionRoots = "PRODUCT")
    public CustomProduct readProductById(@ContextOperation ContextInfo context,
          @PathVariable("id") String productId) {
        return productSvc.readByContextId(productId, context);
    }

}

Common Gotchas

You must override all framework request mappings that share the same path

The most common problem with extending endpoints occurs when multiple request mappings in the framework endpoint have the same path. Usually this happens when mappings have different request methods: A GET for reading an entity by ID and a PUT for replacing it.

Framework Mappings with the same paths but different request methods
@FrameworkGetMapping("/{id}")
@Policy(permissionRoots = "PRODUCT")
public Product readProductById(@ContextOperation ContextInfo context,
      @PathVariable("id") String productId) {
    return productSvc.readByContextId(productId, context);
}

@FrameworkPutMapping("/{id}")
@Policy(permissionRoots = "PRODUCT")
public Product replaceProduct(@ContextOperation(OperationType.UPDATE) ContextInfo context,
      @PathVariable("id") String productId,
      @RequestBody Product updateRequest) {
    updateRequest.setId(id);
    return productSvc.replace(id, updateRequest, context);
}

In this case even if you only want to modify the read endpoint, you will still need to add an override for the replace Note that you can just call super for the additional method.

Example of how to override the previous
@GetMapping("/{id}")
@Policy(permissionRoots = "PRODUCT")
public Product readProductById(@ContextOperation ContextInfo context,
      @PathVariable("id") String productId) {
    // custom code
}

// having to override because the framework method has the same path as the read
@PutMapping("/{id}")
@Policy(permissionRoots = "PRODUCT")
public Product replaceProduct(@ContextOperation(value = OperationType.UPDATE) ContextInfo context,
      @PathVariable("id") String productId,
      @RequestBody Product updateRequest) {
    return super.replaceProduct(context, productId, updateRequest);
}
Note

The problem is when the DispatcherServlet goes looking for a HandlerMapping to resolve the current request, it first consults the default RequestMappingInfoHandlerMapping (which is where all of the @RequestMapping endpoints go). The RequestMappingInfoHandlerMapping essentially only looks for something that matches the URL string and results in a partial mapping for the readProductById() method (which is a GET). Thus, it throws a 405 Method not Allowed rather than continue to send the request downstream to be handled by FrameworkMappingHandlerMapping, which is where all of the @FrameworkMapping methods are.