Broadleaf Microservices

Adding a New External Provider

With Broadleaf’s Composite (Orchestration) services, the framework aggregates information from a variety of different sources to produce a representation of an object suitable for consumption by clients to support a variety of common user experience flows. Broadleaf does this by following an External Provider pattern.

For example, The Catalog Browse Service provides APIs that aggregate data sourced from the Catalog Service, Pricing Service, etc…​ to provide APIs to support typical product detail pages (PDP) and product listing pages (PLP). Other orchestration services within Broadleaf include: Cart Operation Service and the Order Operation Service

In certain situations, you may want to either extend Broadleaf’s existing providers or add a net new External Provider to ultimately augment the payload that is being passed back to the consuming client.

Throughout this tutorial, we will walk through creating a net new External Provider for the Catalog Browse service and add that data to the existing Product payload being returned back.

Building an External Provider

For this example, we will build a provider that "enriches" the product data with additional information from some external mock service.

First, let’s build an interface describing how this external provider will be contributing data to the existing product domain.

package com.broadleafdemo.catalogbrowse.service.provider;

import org.springframework.lang.Nullable;

import com.broadleafcommerce.catalogbrowse.domain.Product;
import com.broadleafcommerce.data.tracking.core.context.ContextInfo;

import java.util.Collection;
import java.util.List;

/**
 * Provider for interfacing with an external "enrichment" service to provide additional data on the
 * product. Typically utilizes {@link WebClient} to make requests to an external REST API.
 */
public interface ProductEnrichmentProvider {

    /**
     * For this simple example, this will just add additional attributes into the provided
     * {@link Product}
     *
     * @param product The product to "enrich"
     * @param contextInfo {@link ContextInfo} from the original request containing relevant
     *        contextual info (e.g. tenant etc...)
     *
     * @return The product with additional attributes attached.
     */
    Product enrichProduct(Product product,
            @Nullable final ContextInfo contextInfo);

    /**
     * For this simple example, this will just add additional attributes for the provided
     * {@link Product Products}
     *
     * @param products The products to "enrich"
     * @param contextInfo {@link ContextInfo} from the original request containing relevant
     *        contextual info (e.g. tenant etc...)
     *
     * @return The products with additional attributes attached.
     */
    <P extends Product> List<P> enrichProducts(Collection<P> products,
            @Nullable final ContextInfo contextInfo);

}

Next, implement the interface to actually integrate with the External Provider. In this example, we’ll simply inject Spring’s WebClient to make a REST call to an external API.

package com.broadleafdemo.catalogbrowse.service.provider.external;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientException;
import com.broadleafcommerce.catalogbrowse.domain.Product;
import com.broadleafcommerce.catalogbrowse.service.provider.external.AbstractExternalProvider;
import com.broadleafcommerce.common.extension.ResponseView;
import com.broadleafcommerce.common.extension.TypeFactory;
import com.broadleafcommerce.data.tracking.core.context.ContextInfo;
import com.broadleafdemo.catalogbrowse.service.provider.ProductEnrichmentProvider;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl;

/**
 * Connects to an external "enrichment" service.
 */
@Slf4j
public class ExternalProductEnrichmentProvider extends AbstractExternalProvider
        implements ProductEnrichmentProvider {

    @Getter(AccessLevel.PROTECTED)
    private final WebClient enrichmentServiceWebClient;

    @Getter(AccessLevel.PROTECTED)
    private final TypeFactory typeFactory;

    /**
     * The base url for an external enrichment service:
     * {@code https://my-external-enrichment-service:1234/enrich}.
     */
    @Getter
    @Setter
    private String url;

    public ExternalProductEnrichmentProvider(
            @Qualifier("enrichmentServiceWebClient") WebClient enrichmentServiceWebClient,
            ObjectMapper mapper,
            TypeFactory typeFactory) {
        super(mapper);
        this.enrichmentServiceWebClient = enrichmentServiceWebClient;
        this.typeFactory = typeFactory;
    }

    @Override
    public Product enrichProduct(Product product, ContextInfo contextInfo) {
        final List<EnrichmentInfo> enrichmentInfos =
                makeRequest(Collections.singletonList(product), contextInfo);

        if (enrichmentInfos.isEmpty()) {
            return product;
        }

        return attachEnrichmentDataToProduct(product, enrichmentInfos);
    }

    @Override
    public <P extends Product> List<P> enrichProducts(Collection<P> products,
            ContextInfo contextInfo) {
        final List<EnrichmentInfo> enrichmentInfos =
                makeRequest(new ArrayList<>(products), contextInfo);

        if (enrichmentInfos.isEmpty()) {
            return products instanceof List ? (List<P>) products : new ArrayList<>(products);
        }

        for (Product p : products) {
            attachEnrichmentDataToProduct(p, enrichmentInfos);
        }

        return products instanceof List ? (List<P>) products : new ArrayList<>(products);
    }

    /**
     * Handles making the actual request to the enrichment service.
     *
     * @param products A list of products to be enriched
     * @param contextInfo {@link ContextInfo} from the original request containing tenant info
     *
     * @return The response from the enrichment service
     */
    protected List<EnrichmentInfo> makeRequest(
            @lombok.NonNull final List<Product> products,
            @Nullable final ContextInfo contextInfo) {
        final ParameterizedTypeReference<List<EnrichmentInfo>> responseType =
                new ParameterizedTypeReference<List<EnrichmentInfo>>() {};

        Object body = contructPayloadBody(products);

        try {
            return Optional.ofNullable(enrichmentServiceWebClient.post()
                    .uri(fromHttpUrl(getUrl())
                            .toUriString())
                    .bodyValue(body)
                    .headers(headers -> headers.putAll(getHeaders(contextInfo)))
                    .accept(MediaType.APPLICATION_JSON)
                    .retrieve()
                    .bodyToMono(responseType)
                    .block())
                    .orElse(new ArrayList<EnrichmentInfo>());
        } catch (WebClientException e) {
            log.error("Encounter an error trying to retrieve enrichment info", e);

            return new ArrayList<>();
        }
    }

    private Product attachEnrichmentDataToProduct(Product product,
            List<EnrichmentInfo> enrichmentInfos) {

        for (EnrichmentInfo info : enrichmentInfos) {
            if (product.getId().equals(info.getProductId())) {
                product.getAttributes().put("enrichmentInfo", info.getData());
            }
        }

        return product;
    }


    private Object contructPayloadBody(List<Product> products) {
        // TODO: contruct the appropriate payload necessary for your request
        return new Object();
    }

    @Data
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonView({ResponseView.class})
    class EnrichmentInfo implements Serializable {

        private static final long serialVersionUID = 1L;

        private String productId;
        private String data;

    }

}
Note
you will want to construct the appropriate WebClient and request payload that is appropriate for calling your API

Extend Existing Catalog Provider

Next - for our example, we’ll want to extend the existing ExternalCatalogProvider to "hydrate" the product with this additional "enriched" data from our external service.

Broadleaf’s existing ExternalCatalogProvider already integrates with Broadleaf’s Catalog Service and Broadleaf’s Pricing Service.

So to enhance this class, we can simply override 2 methods, call super(), and add an additional call to our ProductEnrichmentProvider:

The 2 methods to override are:

  • fetchProductsWithDetails - Invoked from the CatalogBrowseEndpoint when the caller knows they are fetching a product

  • fetchBrowseEntityDetails - Invoked from the CatalogBrowseEndpoint when the caller doesn’t know if it’s a product, category, page etc…​

package com.broadleafdemo.catalogbrowse.service.provider.external;

import org.springframework.web.reactive.function.client.WebClient;

import com.broadleafcommerce.catalogbrowse.domain.BrowseDetailsRequest;
import com.broadleafcommerce.catalogbrowse.domain.BrowseEntityDetails;
import com.broadleafcommerce.catalogbrowse.domain.ProductDetailsRequest;
import com.broadleafcommerce.catalogbrowse.domain.ProductList;
import com.broadleafcommerce.catalogbrowse.service.provider.PricingProvider;
import com.broadleafcommerce.catalogbrowse.service.provider.external.catalog.ExternalCatalogProvider;
import com.broadleafcommerce.common.extension.TypeFactory;
import com.broadleafdemo.catalogbrowse.service.provider.ProductEnrichmentProvider;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;

public class DemoExternalCatalogProvider extends ExternalCatalogProvider {

    @Getter(AccessLevel.PROTECTED)
    private final ProductEnrichmentProvider productEnrichmentProvider;

    public DemoExternalCatalogProvider(WebClient catalogBrowseWebClient,
            ObjectMapper mapper,
            PricingProvider browsePricingProvider,
            ProductEnrichmentProvider productEnrichmentProvider,
            TypeFactory typeFactory) {
        super(catalogBrowseWebClient, mapper, browsePricingProvider, typeFactory);
        this.productEnrichmentProvider = productEnrichmentProvider;
    }

    /**
     * Invoked when the caller knows they are fetching a product
     *
     * @param request
     * @return
     */
    @Override
    public ProductList fetchProductsWithDetails(@NonNull ProductDetailsRequest request) {
        ProductList productList = super.fetchProductsWithDetails(request);
        productList.setProducts(productEnrichmentProvider.enrichProducts(productList.getProducts(),
                request.getContextInfo()));
        return productList;
    }

    /**
     * Invoked when the caller doesn't know if it's a product, category, page etc...
     *
     * @param request
     * @return
     */
    @Override
    public BrowseEntityDetails fetchBrowseEntityDetails(@NonNull BrowseDetailsRequest request) {
        BrowseEntityDetails browseEntityDetails = super.fetchBrowseEntityDetails(request);
        browseEntityDetails.setProducts(productEnrichmentProvider
                .enrichProducts(browseEntityDetails.getProducts(), request.getContextInfo()));
        return browseEntityDetails;
    }
}

Register Your Spring Components

Finally, we need to create a configuration class to register these components and overrides with Spring.

package com.broadleafdemo.catalogbrowse;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;

import com.broadleafcommerce.catalogbrowse.service.autoconfigure.SSLVerificationProperties;
import com.broadleafcommerce.catalogbrowse.service.provider.CatalogProvider;
import com.broadleafcommerce.catalogbrowse.service.provider.PricingProvider;
import com.broadleafcommerce.common.extension.TypeFactory;
import com.broadleafdemo.catalogbrowse.service.provider.ProductEnrichmentProvider;
import com.broadleafdemo.catalogbrowse.service.provider.external.DemoExternalCatalogProvider;
import com.broadleafdemo.catalogbrowse.service.provider.external.ExternalProductEnrichmentProvider;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.function.Supplier;

import javax.net.ssl.SSLException;

@Configuration
@EnableConfigurationProperties(SSLVerificationProperties.class)
public class DemoCatalogBrowseServiceAutoConfiguration {

    @Bean
    @Primary
    public CatalogProvider demoBrowseCatalogProvider(
            @Qualifier("catalogBrowseWebClient") WebClient catalogBrowseWebClient,
            ObjectMapper mapper,
            PricingProvider browsePricingProvider,
            ProductEnrichmentProvider productEnrichmentProvider,
            TypeFactory typeFactory) {
        return new DemoExternalCatalogProvider(catalogBrowseWebClient,
                mapper,
                browsePricingProvider,
                productEnrichmentProvider,
                typeFactory);
    }

    @Bean
    public ProductEnrichmentProvider productEnrichmentProvider(
            @Qualifier("enrichmentServiceWebClient") WebClient enrichmentServiceWebClient,
            ObjectMapper mapper,
            TypeFactory typeFactory) {
        return new ExternalProductEnrichmentProvider(enrichmentServiceWebClient,
                mapper,
                typeFactory);
    }

    @Bean
    public WebClient enrichmentServiceWebClient(
            @Qualifier("oAuth2FilterFunctionSupplier") Supplier<ServletOAuth2AuthorizedClientExchangeFilterFunction> oauth2FilterSupplier,
            ObjectMapper objectMapper,
            SSLVerificationProperties sslVerificationProperties) throws SSLException {
        // TODO: build your own WebClient adequate for your External API
        ExchangeStrategies strategies = ExchangeStrategies
                .builder()
                .codecs(clientDefaultCodecsConfigurer -> {
                    clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(
                            new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
                    clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(
                            new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
                    clientDefaultCodecsConfigurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024);
                }).build();

        WebClient.Builder webClientBuilder = WebClient.builder();

        DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory();
        uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);

        return webClientBuilder
                .uriBuilderFactory(uriBuilderFactory)
                .exchangeStrategies(strategies)
                .apply(oauth2FilterSupplier.get().oauth2Configuration())
                .build();
    }
}

Flex Package spring.factories Note

If you are going to include the Catalog Browse Service jar inside a Flex Package composition, you will want to also make sure to define a spring.factories class allowing your Flex Package application to auto-register your auto-configuration class.

i.e. create a src/main/resources/META-INF/spring.factories file with the following contents:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.broadleafdemo.catalogbrowse.DemoCatalogBrowseServiceAutoConfiguration

Testing

Once you’ve implemented and deployed the above, you can then issue the following request to test and validate that "enrichment" data is added to the response payload.

curl -X 'GET' \
  'https://localhost:8446/api/catalog-browse/products/details?productUris=/hot-sauces/green-ghost' \
  -H 'accept: application/json' \
  -H 'X-Context-Request: {   "applicationId": "2",   "tenantId": "5DF1363059675161A85F576D" }' \
  -H 'X-Price-Context: {   "locale": "en-US",   "currency": "USD" }' \
  -H 'X-Price-Info-Context: {   "priceableTargets": [     {       "targetId": "string",       "targetType": "PRODUCT",       "targetQuantity": 1,       "priceableFields": {         "additionalProp1": 0,         "additionalProp2": 0,         "additionalProp3": 0       },       "vendorRef": "string",       "attributes": {         "additionalProp1": {}       }     }   ],   "priceLists": [     {       "id": "string",       "type": "STANDARD"     }   ],   "skipDetails": false }'

May return a response like:

{
  "products": [
    {
      "id": "product1",
      "sku": "HS-GG-20",
      ...
      "inventoryType": "PHYSICAL",
      "merchandisingProduct": false,
      "active": true,
      "individuallySold": true,
      "enrichmentInfo": "ENRICHED DATA [HS-GG-20]", <-----"Enriched" Attribute Data
      "uri": "/hot-sauces/green-ghost",
      ...
    }
  ],
  "productIdsForMissingEntities": [],
  "productUrisForMissingEntities": []
}