Broadleaf Microservices
  • v1.0.0-latest-prod

Adding a New Export Implementation

You can customize any microservice to use the Broadleaf Common Data Export Library and add an export target implementation for any specific data type.

For a high-level overview of the Broadleaf Data Export Common Library and its components, visit this documentation.

This tutorial will demonstrate each step for adding a new implementation of this library.

Initial Setup

The following steps are only needed for a service that does not have an Out-Of-Box dependency of Data Export.

Adding the Common Data Export dependency

To be able to use the Data Export library, add a new dependency in the pom.xml file of the intended microservice:

    <!--Replace this with the latest version as necessary -->

Extending the JpaAutoConfiguration

Since the export data concept is being introduced to the service, add a new DemoOrderJpaAutoConfiguration that will run before the original OrderJpaAutoConfiguration. Its purpose is to specify the ExportDataRouteSupporting.class as a supportingRouteType.

@JpaDataRoute(boundPropertiesType = OrderProperties.class,
        routePackage = ORDER_ROUTE_PACKAGE, routeKey = ORDER_ROUTE_KEY,
        supportingRouteTypes = {
                // Include all the routes from OrderJpaAutoConfiguration
                // Add the route for export
public class DemoOrderJpaAutoConfiguration {}
Make sure you use the @AutoConfigureBefore annotation.
To learn more about supporting route types, visit the Data Routing Documentation

Schema Changes to support Data Export

Once the broadleaf-common-dataexport library is added as a dependency in the project, it will introduce new tables into the database. To add the changesets responsible for updating the database schema, run UtilitiesIT. After doing so, verify that new changesets are added to your DB changelog, including items such as creating the blc_export table.


Define a ExportSpecification that maps the desired fields to their designated headers in the export document.

public class OrderExportSpecification implements ExportSpecification {

    private final LinkedHashMap<String, String> fieldConfigMap = new LinkedHashMap<>();

    public OrderExportSpecification() {

    public void initSpecification() {
        getFieldConfigMap().put(Fields.NAME, Headers.NAME);
        getFieldConfigMap().put(Fields.CART_TYPE, Headers.CART_TYPE);
        getFieldConfigMap().put(Fields.STATUS, Headers.STATUS);
        getFieldConfigMap().put(Fields.CUSTOMER_ID, Headers.CUSTOMER_ID);
        getFieldConfigMap().put(Fields.ORDER_NUMBER, Headers.ORDER_NUMBER);
        getFieldConfigMap().put(Fields.SUBMIT_DATE, Headers.SUBMIT_DATE);
        getFieldConfigMap().put(Fields.ORDER_ITEMS, Headers.ORDER_ITEMS);
        getFieldConfigMap().put(Fields.ATTRIBUTES, Headers.ATTRIBUTES);

    public static class Headers {
        public static final String NAME = "Name";
        public static final String CART_TYPE = "Cart Type";
        public static final String STATUS = "Status";
        public static final String CUSTOMER_ID = "Customer ID";
        public static final String ORDER_NUMBER = "orderNumber";
        public static final String SUBMIT_DATE = "submitDate";
        public static final String ORDER_ITEMS = "orderItems";
        public static final String ATTRIBUTES = "attributes";

    public static class Fields {
        public static final String NAME = "name";
        public static final String CART_TYPE = "cartType";
        public static final String STATUS = "status";
        public static final String CUSTOMER_ID = "customerId";
        public static final String ORDER_NUMBER = "orderNumber";
        public static final String SUBMIT_DATE = "submitDate";
        public static final String ORDER_ITEMS = "orderItems";
        public static final String ATTRIBUTES = "attributes";

    public boolean canModifyBeanSerializer(Class<?> beanClass) {
        return Order.class.isAssignableFrom(beanClass)
                || Phone.class.isAssignableFrom(beanClass);


Add an extension of AbstractExportRowConverter to process each row for the designated record.

public class OrderRowConverter extends AbstractExportRowConverter<Order> {

    public OrderRowConverter(ExportSpecification specification, ObjectMapper baseMapper) {
        super(specification, baseMapper);

    public Map<String, String> convert(Order source) {
        Map<String, String> result = getMapper().convertValue(source,
                new TypeReference<Map<String, String>>() {});
                .ifPresent(attribute -> {
                    if (!attribute.isEmpty()) {
                                new TypeReference<Map<String, String>>() {}));

        return result;

    protected SimpleModule getSpecificationExportModule(ExportSpecification specification) {
        SimpleModule simpleModule = super.getSpecificationExportModule(specification);
        // The serializer added here is defined below
        simpleModule.addSerializer(new OrderAttributesExportSerializer());

        return simpleModule;

Complex Attributes Serializer

If necessary, add an extension of Jackson’s StdSerializer in order to serialize complex fields.

public class OrderAttributesExportSerializer
        extends StdSerializer<Map<String, Object>> {
    private final String separator;
    private final String valueSeparator;
    private final String keyValueSeparator;

     * Construct the serializer with default separators.
    public OrderAttributesExportSerializer() {
        this("|", ",", ":");

    public OrderAttributesExportSerializer(String separator,
            String valueSeparator,
            String keyValueSeparator) {
        super(Map.class, false);
        this.separator = separator;
        this.valueSeparator = valueSeparator;
        this.keyValueSeparator = keyValueSeparator;

    public void serialize(Map<String, Object> value, JsonGenerator gen, SerializerProvider provider)
            throws IOException {
        if (value != null) {
            String result = value.entrySet().stream()
                    .map(entry -> {
                        Object attrValue = entry.getValue();
                        String stringValue;
                        if (attrValue instanceof Collection) {
                            stringValue = ((Collection<?>) attrValue)
                        } else {
                            stringValue = String.valueOf(attrValue);
                        return String.join(keyValueSeparator, entry.getKey(), stringValue);

Export Processor

Define a DefaultExportTarget to be utilized by the Processor, and later used to specify the type of export when initiated by the endpoint.

public enum DefaultExportTarget {

Define an OrderExportProcessor that implements ExportProcessor and overrides its methods. This component is responsible for processing the Export to be generated from the request, and it may be extended with additional logic for a custom export.

public class OrderExportProcessor implements ExportProcessor<Order> {

    private final OrderService<Order> orderService;
    private final TypeFactory typeFactory;
    private final FilterParser<Node> filterParser;
    private final OrderRowConverter orderRowConverter;
    private final ContextRequestHydrator hydrator;

    public boolean canHandle(Export export) {
        return StringUtils.equals(,

    public LinkedHashSet<String> getHeaders() {
        return orderRowConverter.getHeaders();

    public ReadRecordsResponse<Order> readRecordsToProcess(Export export) {
        ContextInfo contextInfo = buildQueryContextInfo(export);

        Node filters = getFilters(export);
        final Stream<Order> result;

        // If the target implementation is intended to support Export#inclusions and Export#exclusions, filtration on those should be handled here. This example does not support those filters.
        filters = filters == null ? new EmptyNode() : filters;

        // For demonstration purposes in this tutorial, we will utilize the following naive implementation that will eagerly load all records into memory
        result = orderService.readAll(filters, contextInfo).stream();

        return ReadRecordsResponse.success(result);

    public RowGenerationResponse generateRows(List<Order> batchToProcess, Export export) {
        return RowGenerationResponse.success(

    protected Node getFilters(Export export) {
        if (StringUtils.isBlank(export.getFilterString())) {
            return null;

        return filterParser.parse(export.getFilterString());

    protected ContextInfo buildQueryContextInfo(Export export) {
        ContextRequest contextRequest = hydrator.hydrate(buildContextRequestFromExport(export));
        if (StringUtils.isEmpty(contextRequest.getCustomerContextId())) {
        ContextInfo contextInfo = new ContextInfo(OperationType.READ);

        return contextInfo;

    protected ContextRequest buildContextRequestFromExport(Export export) {
        ContextRequest contextRequest = typeFactory.get(ContextRequest.class);
        return contextRequest;
A production implementation should instead leverage a query method that will actually stream records incrementally from the datastore. For examples of this, see BroadleafPagingStreams#streamBuilder from the DataTracking library.
Visit the Export domain’s inclusions and exclusions Javadocs for more information

Spring Configuration

In order services, add a new AutoConfiguration class to register the OrderExportSpecification, OrderRowConverter, and OrderExportProcessor beans.

public class DemoOrderExportAutoConfiguration {

    public static class Specifications {

        public OrderExportSpecification orderExportSpecification() {
            return new OrderExportSpecification();

    public static class Converters {

        public OrderRowConverter orderRowConverter(
                OrderExportSpecification orderExportSpecification,
                ObjectMapper objectMapper) {
            return new OrderRowConverter(orderExportSpecification, objectMapper);

    public OrderExportProcessor orderExportProcessor(
            OrderService<Order> orderService,
            TypeFactory typeFactory,
            FilterParser<Node> filterParser,
            OrderRowConverter orderRowConverter,
            ContextRequestHydrator hydrator) {
        return new OrderExportProcessor(orderService,
You may need to enable your auto-configuration class via spring.factories as discussed in this documentation.

Export Endpoint

Add a custom endpoint and inject ExportManager, ExportService, and ExportDownloadService. For this example, add a GET endpoint to read an existing export record. Also, add a POST endpoint to generate a new record. This endpoint is responsible for passing an exportRequest into exportManager.initiateExport() to trigger the workflow in the Export Common Library. Finally, add a GET endpoint that will call the ExportDownloadService to retrieve an already existing export record as a file.

public class OrderExportEndpoint {

    private final ExportManager exportManager;
    private final ExportService<Export> exportService;
    private final ExportDownloadService exportDownloadService;

    public ResponseEntity<?> readExportById(
            @PathVariable(value = "id") String exportId,
            @ContextOperation(OperationType.READ) ContextInfo contextInfo) {
        Export export = exportService.readById(exportId);
        return ResponseEntity.ok()

    @PostMapping(value = "/exports/orders",
            consumes = MediaType.APPLICATION_JSON_VALUE)
    public Export exportOrders(
            @RequestParam(value = "cq", defaultValue = "", required = false) String filterString,
            @RequestBody ExportRequest exportRequest,
            @ContextOperation(value = OperationType.READ) ContextInfo context) {
        return exportManager.initiateExport(exportRequest,
      , filterString, context);

    public ResponseEntity<StreamingResponseBody> downloadOrderExport(
            @PathVariable("id") String exportId,
            @ContextOperation(value = OperationType.READ) ContextInfo context,
            HttpServletResponse response) {
        Export export = exportService.readById(exportId);
        if (!CommonExportUtils.isExportBelongsToCurrentApplication(export, context) ||
                !CommonExportUtils.isExportFinished(export)) {
            return ResponseEntity.notFound().build();

        Optional<Resource> exportFile = exportDownloadService.getExportFile(exportId);
        return ResponseEntity.ok()
                        String.format("attachment; filename=%s",
                .body(outputStream -> IOUtils.copy(exportFile.get().getInputStream(),

The above demonstrates fundamental use of the export library. In your implementation, you may want to add other endpoints to read a filtered collection of exports, initiate an export for a specific list of entities, return a paged response, implement custom permissions, etc.

Web Auto Configuration

Register the endpoint adding a configuration class that calls EnableBroadleafControllers with the @ComponentScan annotation specifying the new endpoint class.

@ComponentScan(basePackageClasses = OrderExportEndpoint.class)
public class DemoOrderWebAutoConfiguration {}
You may need to enable your auto-configuration class via spring.factories as discussed in this documentation.


Add the following properties to your flex package’s configuration file (e.g. min’s application-defaults.yml).

          group: order-export
          destination: exportRequest
          destination: exportRequest
          destination: cloneProduct
        retry-cluster-service-namespace: process-export-request-order

Expose the Endpoint in the Admin using Metadata

For this tutorial, we will extend the already existing Orders grid in the admin using metadata.

To do this, find the augmentation key for the view (in this case, order:orders:browse), and get the component you wish to customize (in this case, the default grid). Last, configure the export modal by calling .exportGridAction() as seen below:

public class OrderMetadataAutoConfiguration {
    public class OrderProps {
        public final String ORDER_AUG_KEY = "order:orders:browse";
        public final String ORDER_EXPORTS = "/search/exports/orders";
        public final String ORDER_EXPORT = ORDER_EXPORTS + "/${exportId}";
        public final String ORDER_EXPORT_DOWNLOAD = "/api" + ORDER_EXPORT + "/download";

    public ComponentSource addExportImplementationToGrid() {
        return registry -> {
            EntityBrowseView<?> browseView =
                    (EntityBrowseView<?>) registry.get(OrderProps.ORDER_AUG_KEY);

            EntityGridView<?> grid = browseView.getDefaultGrid();

            grid.exportGridAction(CustomerScopes.CUSTOMER, exportAction -> exportAction
                    .startExportEndpoint(startEndpoint -> startEndpoint
                    .readExportEndpoint(readEndpoint -> readEndpoint
                    .label("Export Orders"));
You may need to enable your auto-configuration class via spring.factories as discussed in this documentation.
Follow these tutorials to learn more about how to customize the admin using Metadata.

Testing in the Broadleaf Admin

Let’s test out our new export in the admin.

  • Visit the Orders page. You will see the newly added export button in the grid.

New Export Button
  • Click on 'Export'. You will see the export orders modal, where you may overwrite the pre-populated file name field and initiate a new export.

Export Orders Modal
  • Click on 'Submit'.

  • Once the export has been created successfully, you will see two notifications. Download the export file.

Download File
This link hits the previously added downloadOrderExport endpoint. You may customize this behavior to fit your needs (e.g. adding a page of exports with a download option for each record).
  • Take a look at the CSV file generated from your orders!

CSV File Preview