Broadleaf Microservices
  • v1.0.0-latest-prod

Extending Out-of-Box Domain [DEPRECATED]

Important
This guide has been deprecated in favor of the Microservices Concepts project. To take advantage of the new Extensibility Patterns outlined in this project, you will need to upgrade to the latest broadleaf common library dependencies listed here which can be applied to any installation running Release Train 1.7.3 or above. The patterns outlined in this article are still applicable to those running libraries older than those identified above.

Extending the Repository Domain

In this tutorial, we’re going to cover how to extend the actual JpaProduct object in the Catalog Microservice to track a couple new attributes directly on the object.

Note
Broadleaf leverages Spring Data for entity persistence. Spring Data supports polymorphism as part of the data interaction, which facilitates standard Java OOP extension of out-of-the-box Broadleaf repository domain classes. Fields may be added in extensions to support additional business requirements. Also, mutator methods can be overridden for additional customization. Queries through existing Broadleaf repositories should return the more derived repository domain as appropriate. As well, creation of entity state should be accurate to the more derived type.
package com.broadleafdemo.catalog.jpa.domain;

import org.modelmapper.ModelMapper;
import org.springframework.lang.NonNull;
import com.broadleafcommerce.catalog.domain.product.Product;
import com.broadleafcommerce.catalog.provider.jpa.domain.product.JpaProduct;
import com.broadleafcommerce.common.jpa.JpaConstants;
import com.broadleafdemo.catalog.domain.MyProduct;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Entity
@Data
@EqualsAndHashCode(callSuper = true)
@Table(name = "MY_PRODUCT")
public class MyJpaProduct extends JpaProduct {

    private static final long serialVersionUID = 1L;

    @Column(name = "LINE_CODE")
    private String lineCode;

    @Column(name = "CARE_INSTRUCTIONS", length = JpaConstants.TEXT_LENGTH)
    private String careInstructions;

    @Override
    @NonNull
    public ModelMapper fromMe() {
        ModelMapper mapper = super.fromMe();
        mapper.getTypeMap(JpaProduct.class, Product.class)
                .include(MyJpaProduct.class, MyProduct.class);

        mapper.getTypeMap(MyJpaProduct.class, MyProduct.class)
                .addMapping(MyJpaProduct::getLineCode, MyProduct::setLineCode)
                .addMapping(MyJpaProduct::getCareInstructions,
                        MyProduct::setCareInstructions);

        return mapper;
    }

    @Override
    public ModelMapper toMe() {
        ModelMapper mapper = super.toMe();
        mapper.getTypeMap(Product.class, JpaProduct.class)
                .include(MyProduct.class, MyJpaProduct.class);

        mapper.getTypeMap(MyProduct.class, MyJpaProduct.class)
                .addMapping(MyProduct::getLineCode, MyJpaProduct::setLineCode)
                .addMapping(MyProduct::getCareInstructions,
                        MyJpaProduct::setCareInstructions);
        return mapper;
    }


    @Override
    public Class<?> getBusinessDomainType() {
        return MyProduct.class;
    }

}
Note
MyProduct.class is the projection or "business" domain which we’ll be defining in the following step

Next, you’ll want to inform the system of your new entity. You can do this by specifying a @JpaEntityScan for your particular package. It may look something like this:

package com.broadleafdemo.catalog.jpa.autoconfigure;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Configuration;
import com.broadleafcommerce.catalog.provider.jpa.autoconfigure.CatalogJpaAutoConfiguration;
import com.broadleafcommerce.common.jpa.data.entity.JpaEntityScan;

import static com.broadleafcommerce.catalog.provider.RouteConstants.Persistence.CATALOG_ROUTE_PACKAGE;

@Configuration
@JpaEntityScan(basePackages = "com.broadleafdemo.catalog.jpa.domain",
        routePackage = CATALOG_ROUTE_PACKAGE)
@AutoConfigureAfter(CatalogJpaAutoConfiguration.class)
public class MyCatalogJpaAutoConfiguration {

    //other beans and configuration here

}
Note
The important step here is to make sure that the @JpaEntityScan is referencing the correct package where your extensions exist (e.g. com.broadleafdemo.catalog.jpa.domain)

Extending the Projection Domain

In this section, we’re going to cover how to extend the Product projection domain so that our new properties can be used by downstream clients and other projection domains.

As with the MyJpaProduct extension, We’re going to replicate and add the same simple properties to our Product Projection domain.

package com.broadleafdemo.catalog.domain;

import com.broadleafcommerce.catalog.domain.product.Product;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MyProduct extends Product {

    private String lineCode;
    private String careInstructions;

}

Next, you’ll want to inform the system of your new projection extension using a TypeSupplier. You can do this by specifying a custom bean in your configuration:

package com.broadleafdemo.catalog.service.autoconfigure;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.broadleafcommerce.catalog.domain.product.Product;
import com.broadleafcommerce.common.extension.TypeSupplier;
import com.broadleafdemo.catalog.domain.MyProduct;

@Configuration
public class MyCatalogServicesAutoConfiguration {

    @Bean
    public TypeSupplier myProductSupplier() {
        return () -> new TypeSupplier.TypeMapping(Product.class,
                MyProduct.class);
    }

}

Adding a Liquibase Changelog

We recommend utilizing liquibase to manage schema changes, so you’ll want to add a new changelog for our extension and ensure the system is aware of our changes.

Broadleaf has provided a utility class that will automatically look at the state of your current application’s changelogs and generate any missing change set for you.

Set up your Liquibase Change Log Structure

Create a file called catalog.flexdemo.postgresql.changelog-master.xml in the following directory: services/catalog/src/main/resources/db/changelog/ (if not already present in your starter project).

The contents of the file should look something like this initially:

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

    <!-- include the master baseline changelog from the services dependency JAR -->
    <include file="db/changelog/catalog.demo.postgres.changelog-master.yaml" />
    <!-- include any applicable initial drop change logs -->

    <!-- include file="db/changelog/service.postgresql.drop.changelog-X.X.X.xml" -->
    ...

    <!-- Client Schema Extensions Can Be Specified/Generated Below -->
</databaseChangeLog>

Generate Missing Change Sets

To engage the change set generation capabilities, you will first need to create an integration test class under services/catalog/src/test/java/com/broadleafdemo/catalog

Create a UtilitiesIT class like below (if not already present in the starter):

import org.junit.jupiter.api.Nested;
import org.springframework.test.context.TestPropertySource;

import com.broadleafcommerce.common.jpa.schema.SchemaCompatibiltyUtility;

/**
 * Verify that we can start up against RDBMS using the known schema configuration. The
 * {@code Utility} test class is intended for use by developers to keep JPA entity changes in sync
 * with the liquibase change logs.
 */
public class UtilitiesIT {

    // @formatter:off
    /**
     * Execute these utility tests directly from the IDE in order to update the liquibase
     * change logs for each supported RDBMS platform based on the current JPA entity state. Updated
     * Liquibase change logs are emitted at src/main/resources/db/changelog.
     */
    // @formatter:on
    public static class AllUtilities {

        @TestPropertySource(properties = {"spring.liquibase.enabled=false", "service.key=catalog",
                "client.prefix=flexdemo"})
        @Nested
        public class PostgresUtility extends SchemaCompatibiltyUtility.PostgresUtilityProvider {}

    }

}

Once you have this class in place, run this class in your IDE as an integration test.

This should update the master changelog file referenced earlier: src/main/resources/db/changelog/catalog.flexdemo.postgresql.changelog-master.xml with the missing changes that need to be applied to the DB (e.g. the MY_PRODUCT table)

Demo Data MyProduct Updates (Tutorial Specific Instructions)

The Broadleaf starter loads in demo "hot sauce" data by default. Because we’ve extended Product in this tutorial, we’ll also want to update the seed data to account for our extended BLC_PRODUCT table in the Catalog service. To do this, we can easily add a liquibase update script here: /services/catalog/src/main/resources/sql/update-demo-data.sql

-- liquibase formatted sql
-- changeset broadleaf:update-demo-data

insert into my_product (id) select id from blc_product;

and then reference this in your liquibase master change log. /services/catalog/src/main/resources/db/changelog/catalog.flexdemo.postgresql.changelog-master.yaml

databaseChangeLog:
  - include:
      file: db/changelog/catalog.flexdemo.postgresql.changelog-master.xml
  - include:
      file: sql/update-demo-data.sql

In some cases, you may also wish to expose your extended repository projection to other dependent projection objects. An example in the CatalogService would include the ProductDetails projection domain. This domain is used to facilitate building a product structure containing curated information needed for a typical product details page shown in a commerce-facing storefront app (as opposed to info needed to support a product management screen)

To do this, you’ll want to extend the base ProductDetails domain like below:

import com.broadleafcommerce.catalog.domain.product.IncludedProduct;
import com.broadleafcommerce.catalog.domain.product.Product;
import com.broadleafcommerce.catalog.domain.product.commerce.ProductDetails;
import com.broadleafcommerce.catalog.domain.product.option.ProductOption;
import com.broadleafcommerce.common.extension.ResponseView;
import com.mycompany.catalog.domain.MyProduct;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonView;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.experimental.Delegate;

@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties({"contextState", "defaultPrice", "msrp", "salePrice"})
@JsonView({ResponseView.class})
public class MyProductDetails extends ProductDetails {

    private static final long serialVersionUID = 1L;

    @JsonIgnore
    @Delegate(excludes = ExcludedProductMethods.class)
    private MyProduct product;

    @Override
    public void setProduct(Product product) {
        this.product = (MyProduct) product;
        super.setProduct(product);
    }

    @Override
    public MyProduct getProduct() {
        return product;
    }

    @Data
    @Accessors(chain = true)
    @AllArgsConstructor
    private abstract static class ExcludedProductMethods {
        private List<IncludedProduct> includedProducts;

        private List<ProductOption> options;

        public abstract String getMetaDescription();

        public abstract String getMetaTitle();
    }


}

Next, you’ll want to inform the system of your new projection extension using a TypeSupplier. You can do this by specifying a custom bean in your configuration:

public class MyCatalogServicesAutoConfiguration {

    ...

    @Bean
    public TypeSupplier myProductDetails() {
        return () -> new TypeSupplier.TypeMapping(ProductDetails.class,
                MyProductDetails.class);
    }

}

Additional Spring Components

In this section, we’ll walk through extending other interesting components in the framework which might be related to the base Product extension outlined above.

Product Export Customization

We’re going to extend framework’s Product Export Row Converter in order to add the simple additional property that we’ve added to the repository domain. This will allow the default Product Export jobs to utilize the new extended attributes when producing the CSV file.

import com.broadleafcommerce.catalog.dataexport.converter.DimensionsExportRowConverter;
import com.broadleafcommerce.catalog.dataexport.converter.ProductExportRowConverter;
import com.broadleafcommerce.catalog.dataexport.converter.ToStringConverter;
import com.broadleafcommerce.catalog.dataexport.converter.WeightExportRowConverter;
import com.broadleafcommerce.catalog.dataexport.converter.support.ConversionUtils;
import com.broadleafcommerce.catalog.dataexport.specification.ProductExportSpecification;
import com.broadleafcommerce.catalog.domain.product.Product;
import com.broadleafsamples.tutorials.services.catalog.provider.jpa.domain.TutorialJpaProduct;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.LinkedHashSet;
import java.util.Map;

import static com.broadleafcommerce.common.extension.reflection.InvocationUtils.withExample;

public class MyProductExportRowConverter extends ProductExportRowConverter {

    public MyProductExportRowConverter(ProductExportSpecification specification,
                                             ObjectMapper objectMapper,
                                             ToStringConverter<Object> toStringConverter,
                                             DimensionsExportRowConverter dimensionsExportRowConverter,
                                             WeightExportRowConverter weightExportRowConverter) {
        super(specification, objectMapper, toStringConverter, dimensionsExportRowConverter, weightExportRowConverter);
    }

    @Override
    public LinkedHashSet<String> getHeaders() {
        LinkedHashSet<String> headers = super.getHeaders();
        headers.add(MyFields.LINE_CODE);
        return headers;
    }

    @Override
    public Map<String, String> convert(Product source) {
        Map<String, String> result = super.convert(source);
        ConversionUtils.putIfNotNull(MyFields.LINE_CODE,
                ((MyProduct)source).getLineCode(), result);
        return result;
    }

    public static class MyFields {
        public static final String LINE_CODE = "lineCode";
        public static final String CARE_INSTRUCTIONS = "careInstructions";
    }

}
Tip

Broadleaf provides some shortcut extension patterns that allows "auto-generation" of projection domains. In simple cases, it may be enough to just extend the JpaProduct repository domain. However, in cases where you need more control of the projection object (or there are other projections that reference it) you will also want to define a MyProduct extends Product class (which was demonstrated above).

In the simple cases, where Broadleaf builds the projection for you, you can utilize the withExample() utility method as below:

ConversionUtils.putIfNotNull(MyFields.MY_PROPERTY, withExample(MyProduct.class).andTarget(source).getMyProperty(), result);