Broadleaf Microservices

Adding a New Data Driven Enum Field to Product

As of Broadleaf 1.5, a new DataDrivenEnum concept was introduced to dynamically manage enums via the admin. In this tutorial, we’re going to cover how to extend the JpaProduct object in the CatalogServices to add a new DataDrivenEnum field. From there, we’ll discuss how the field is indexed, translated, and added to ProductDetails. This tutorial will add customizations in CatalogServices, MetadataServices, SearchServices, and IndexerServices.

Note
If unfamiliar with the DataDrivenEnum concept, you can learn more by referencing Documentation on DataDrivenEnums and DataDrivenEnum’s Data Model

Extending the Product Domain

Let’s say we want to add a new field to Product to represent its manufacturer. We will use the DataDrivenEnum domain to manage all the available values via the admin.

Extending the Projection Domain

First, we need to extend the Product projection domain.

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

    private static final long serialVersionUID = 1L;

    /**
     * @param manufacturer the manufacturer of this product
     * @return the manufacturer of this product
     */
    @Nullable
    private DataDrivenEnum manufacturer;
}

Next, we need to inform the system of our new projection extension using a TypeSupplier. We can do this by declaring a new bean.

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

Extending the Repository Domain

Next, we need to extend the JpaProduct domain.

Note
Broadleaf leverages Spring Data for entity persistence. You can learn more details by referencing Extending the Out-of-Box Repository Domain
@Entity
@Table(name = "MY_PRODUCT")
@Data
@EqualsAndHashCode(callSuper = true)
public class MyJpaProduct extends JpaProduct {

    private static final long serialVersionUID = 1L;

    @Column(name = "MANUFACTURER_CONTEXT_ID", length = CONTEXT_ID_LENGTH)
    @Convert(converter = UlidConverter.class)
    private String manufacturerContextId; (1)

    @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) (2)
                .addMappings(mapping -> mapping
                        .when(Conditions.isNotNull())
                        .<String>map(MyJpaProduct::getManufacturerContextId,
                                (product, manufacturerId) -> product
                                        .getManufacturer()
                                        .setId(manufacturerContextId)));

        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) (3)
                .addMappings(mapping -> mapping
                        .when(Conditions.isNotNull())
                        .map(product -> product.getManufacturer().getId(),
                                MyJpaProduct::setManufacturerContextId));
        return mapper;
    }

    @Override
    public Class<?> getBusinessDomainType() {
        return MyProduct.class;
    }
}
  1. In the entity persistence domain, we will persist the id of the DataDrivenEnum instead of the whole object

  2. Since the projection domain uses the DataDrivenEnum object, while the persistence domain only has the id, we need to map the id onto the persistence domain

  3. Similar to #2, we need to define a toMe mapping for the id of the manufacturer onto the projection domain

Next, we will want to inform the system of our new entity. We do this by adding a @JpaEntityScan annotation targeting our package.

@Configuration
@JpaEntityScan(basePackages = "com.broadleafdemo.catalog.jpa.domain",
        routePackage = CATALOG_ROUTE_PACKAGE)
@AutoConfigureAfter(CatalogJpaAutoConfiguration.class)
public class MyCatalogJpaAutoconfiguration {}
Important
Make sure that the @JpaEntityScan is referencing the correct package where your extensions exist (i.e. com.broadleafdemo.catalog.jpa.domain)

Registering Configuration Classes for Extended Domains

Since we added the configuration classes for the TypeSupplier and the JpaEntityScan, we need to add a META-INF/spring.factories file in our catalog services.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.broadleafdemo.catalog.service.autoconfigure.MyCatalogServicesAutoConfiguration,\
  com.broadleafdemo.catalog.jpa.autoconfigure.MyCatalogJpaAutoconfiguration

Extending the Metadata

For all admin interactions that use our new DataDrivenEnum, we’ll need to declare an enum type. This ensures that we only interact with manufacturer options for our manufacturer field.

First, we need to define a SelectOption.SelectOptionEnum for the type selections in your MetadataServices.

public enum MyDataDrivenEnumTypeOptionEnum implements SelectOption.SelectOptionEnum {

    MANUFACTURER("data-driven-enum.type.enum.manufacturer"); (1)

    MyDataDrivenEnumTypeOptionEnum(String label) {
        this.label = label;
    }

    private String label;

    @Override
    public String label() {
        return label;
    }

    /**
     * Factory method to generate a set of {@link SelectOption} from this enumeration.
     *
     * @return the set of options
     */
    public static List<SelectOption> toOptions() { (2)
        return SelectOption.fromEnums(values());
    }
}
  1. Metadata label which we will define later

  2. Methods to be used when we define the metadata extension for DataDrivenEnum

Next, we need to extend DataDrivenEnum’s metadata to add the new MANUFACTURER type, as well as adding the manufacturer field to Product’s metadata.

Note
If you have already extended the metadata for the out-of-box DataDrivenEnum fields in Product by following this documentation, you can add the following metadata in the same class
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(CatalogMetadataProperties.class)
@AutoConfigureAfter(CatalogServicesMetadataAutoConfiguration.class)
public class MyCatalogMetadataAutoConfiguration {

    private final CatalogMetadataProperties properties; (1)

    @UtilityClass
    public class DemoProductProps { (2)
        public final String MANUFACTURER = "manufacturer";
    }

    @UtilityClass
    public class DataDrivenEnumTypes {
        public final String MANUFACTURER = "MANUFACTURER";
    }

    @Bean
    public ComponentSource addDataDrivenEnumType() { (3)
        return registry -> {

            CreateEntityView<?> dataDrivenEnumCreate = (CreateEntityView<?>) registry
                    .get(DataDrivenEnumIds.CREATE);

            UpdateEntityView<?> dataDrivenEnumUpdate = (UpdateEntityView<?>) registry
                    .get(DataDrivenEnumIds.UPDATE);

            SelectField<?> enumTypeSelectForCreate =
                    (SelectField<?>) dataDrivenEnumCreate.getGeneralForm()
                            .getField(DataDrivenEnumProps.TYPE);

            SelectField<?> enumTypeSelectForUpdate =
                    (SelectField<?>) dataDrivenEnumUpdate.getGeneralForm()
                            .getField(DataDrivenEnumProps.TYPE);

            enumTypeSelectForCreate
                    .options(MyDataDrivenEnumTypeOptionEnum.toOptions());
            enumTypeSelectForUpdate
                    .options(MyDataDrivenEnumTypeOptionEnum.toOptions());
        };
    }

    @Bean
    public ComponentSource addProductDataDrivenEnumFields() { (4)
        return registry -> {
            for (DefaultProductType type : getAvailableProductTypes()) { (5)
                UpdateEntityView<?> productUpdate = (UpdateEntityView<?>) registry
                        .get(String.format(ProductIds.UPDATE, type.name()));

                Group<?> merchandisingGroup = productUpdate.getForm(ProductForm.STORE_FRONT)
                        .getGroup(ProductGroups.MERCHANDISING);

                merchandisingGroup
                        .addField(DemoProductProps.MANUFACTURER,
                                createDataDrivenEnumLookup(DataDrivenEnumTypes.MANUFACTURER,
                                        "product.fields.manufacturer")
                                                .order(1400)); (6) (7)
            }
        };
    }

    /**
     * Retrieve the list of active product types.
     *
     * @return a list of active product types
     */
    protected List<DefaultProductType> getAvailableProductTypes() { (8)
        return Arrays.stream(values())
                .filter(t -> properties.getActiveProductTypes().contains(t.name()))
                .collect(Collectors.toList());
    }
}
  1. Metadata properties for the CatalogServices. This is needed in our configuration class to retrieve all the active product types. Using our knowledge of the active product types, we can register our new field with all types, or a few specific types.

  2. The manufacturer property on Product.

  3. The bean that adds the new MANUFACTURER type to the DataDrivenEnum create and update forms, which will look like: Manufacturer Data Driven Enum Type

  4. The bean that adds the new manufacturer field to Product’s update form under the "Merchandising" group, which will look like: Product’s Manufacturer Field

  5. Adds the new manufacturer field to Product’s update form for all active product types.

  6. When adding the manufacturer field, we are using helper methods from DataDrivenEnumLookupHelpers to create a metadata lookup for all DataDrivenEnums with the type of MANUFACTURER.

  7. product.fields.manufacturer is the field label.

  8. Method to retrieve all the active product types based on CatalogMetadataProperties.

Adding Field Labels

Next, we need to create a my-catalog.properties file to declare the values for our field labels:

# Fields
## Product Fields
product.fields.manufacturer=Manufacturer

## Data Driven Enum Type
data-driven-enum.type.enum.manufacturer=Manufacturer

Then, that messages file needs to be registered:

public class MyCatalogMetadataMessages implements MetadataMessagesBasename {

    @Override
    @NonNull
    public String getBasename() {
        return StringUtils.join(Arrays.asList("messages/my-catalog"), ",");
    }
}

Registering Configuration Classes for Metadata

Since we added the configuration classes for the metadata and messages, we need to add a META-INF/spring.factories file in our metadata services.

com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
  com.broadleafdemo.metadata.catalog.i18n.MyCatalogMetadataMessages

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.broadleafdemo.metadata.catalog.autoconfigure.MyCatalogMetadataAutoConfiguration
Important
Make sure that the custom metadata is defined in the MetadataServices (i.e. MyProject/services/metadata) and not CatalogServices

Hydrating the New Field

Since our persistence domain MyJpaProduct only persists the id of the DataDrivenEnum that represents the manufacturer, our projection domain MyProduct will need to be hydrated.

Extending DefaultProductHydrationService

To hydrate our manufacturer field, we need to extend the DefaultProductHydrationService. This process is quite simple. We’ll only need to define a constructor and override two methods: one to determine all of the references, and one to populate Product’s manufacturer fields.

@Slf4j
public class MyProductHydrationService extends DefaultProductHydrationService {

    public MyProductHydrationService(ProductService<Product> productService,
            ProductAssetService<ProductAsset> productAssetService,
            VariantService<Variant> variantService, CategoryService<Category> categoryService,
            CategoryProductService<CategoryProduct> categoryProductService,
            DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService, TypeFactory typeFactory) {
        super(productService, productAssetService, variantService, categoryService,
                categoryProductService, dataDrivenEnumService, typeFactory);
    }

    @Override
    protected void determineReferences(@lombok.NonNull Product product,
            @lombok.NonNull ProductReferences references) {
        super.determineReferences(product, references);
        determineReferences(((MyProduct) product).getManufacturer(), references);
    }

    @Override
    protected void setProductDataDrivenEnumFieldsIfFound(Product product,
            ResolvedProductReferences foundItems) {
        super.setProductDataDrivenEnumFieldsIfFound(product, foundItems);
        applyHydration(((MyProduct) product).getManufacturer(), "Product#manufacturer", foundItems);
    }
}

Then, we need to register the new bean in MyCatalogServicesAutoConfiguration.

@Bean
@Primary
public ProductHydrationService myProductHydrationService(
        ProductService<Product> productService,
        ProductAssetService<ProductAsset> productAssetService,
        VariantService<Variant> variantService,
        CategoryService<Category> categoryService,
        CategoryProductService<CategoryProduct> categoryProductService,
        DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService,
        TypeFactory typeFactory) {
    return new MyProductHydrationService(
            productService,
            productAssetService,
            variantService,
            categoryService,
            categoryProductService,
            dataDrivenEnumService,
            typeFactory);
}

Adding Liquibase Change Sets

Liquibase Change Log Structure

Before going into this section, we recommend making sure that your Liquibase change log structure is set up correctly by going through Set Up Your Liquibase Change Log Structure.

Generating Change Sets

Once you’ve verified that your Liquibase configuration is correct, you can generate change sets reflecting our schema updates for the MyJpaProduct extension by following Generating Missing Change Sets.

After the change sets are generated, your change log file should look something like this:

<?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.postgres.changelog-master.yaml" />

    <!-- include any applicable initial drop change logs -->

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

    <!-- Client Schema Extensions Can Be Specified/Generated Below -->
    <changeSet author="syu (generated)" id="1617660978265-1" labels="version-1.5.0">
        <createTable tableName="my_product">
            <column name="manufacturer_context_id" type="varchar(36 BYTE)" />
            <column name="id" type="VARCHAR(36)">
                <constraints nullable="false" primaryKey="true"
                    primaryKeyName="my_product_pkey" />
            </column>
        </createTable>
    </changeSet>
    <changeSet author="syu (generated)" id="1617660978265-2" labels="version-1.5.0">
        <addForeignKeyConstraint baseColumnNames="id"
            baseTableName="my_product" constraintName="fk733bkfcjie94e4ksotn5jjivv"
            deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
            referencedColumnNames="id" referencedTableName="blc_product" validate="true" />
    </changeSet>
</databaseChangeLog>

Populating Data for the New Table

Now that the my_product table is created, we need to we have a record for each existing product that we have in blc_product. If this is not done, then Hibernate will fail to gather the existing products.

To populate the data, we can simply add a Liquibase script in the CatalogServices, i.e. MyProject/services/catalog/src/main/resources/sql/populate-data-for-my-product.sql.

-- liquibase formatted sql
-- changeset broadleaf:populate-data-for-my-product

-- This will add all product ids to my_product and null for the manufacturer_context_id field
insert into catalog.my_product (id) select id from catalog.blc_product;

Lastly, we need to reference this script in the Liquibase master changelog, MyProject/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 any client change logs and additional Broadleaf upgrade change logs (drops etc...) below
  - include:
      file: populate-data-for-my-product.sql

Optional Changes to Use the New Field in Your Customer-Facing Storefront

With all the changes above, we are able to create new DataDrivenEnums with the type of MANUFACTURER, and use them to populate the Product’s manufacturer field. If we want to use the new field in the storefront, such as displaying the manufacturer on the product details page, search faceting based on this field, etc., then additional changes are needed.

Note
If unfamiliar with search fields or indexing, you can learn more by referencing SearchServices Documentation

Indexing the New Field

In order to search or facet the search results based on Product’s manufacturer, we need to index the field. We’ll start by indexing the DataDrivenEnum’s display value, since it’s more user-friendly.

Note
The DataDrivenEnum’s value is meant for system processing and management in the backend, while the display value is meant for customer-facing usages

Defining a Search Field

To index the display value of manufacturer, there are a few changes we need to make.

First, we need to create a sql script to create a search field in the SearchServices, i.e. MyProject/services/search/src/main/resources/sql/create-manufacturer-field.sql.

-- liquibase formatted sql
-- changeset broadleaf:create-manufacturer-field

insert into search.blc_field (id,
                       context_id,
                       label,
                       indexable_type,
                       multi_valued,
                       property_path,
                       combined,
                       property_paths,
                       delimiter,
                       abbreviation,
                       translatable,
                       variants,
                       faceted,
                       facet_label,
                       facet_display_order,
                       facet_multi_select,
                       facet_ranged,
                       facet_ranges,
                       facet_rule,
                       facet_variant_type,
                       sortable,
                       sort_label,
                       sort_display_order,
                       sort_variant_type,
                       searchable,
                       field_queries,
                       trk_archived,
                       trk_level,
                       trk_sandbox_archived,
                       trk_tenant_id)
values ('01FDT3MHVTEG9Z0GSFZMW81WF8',
        'myProductManufacturerField',
        'Manufacturer Display Value',
        'PRODUCT',
        'N',
        'manufacturer.displayValue',
        'N',
        NULL,
        NULL,
        'manufacturerDisplayValue',
        'Y', (1)
        '[{"type":"STRING","includeInResponse":true}]',
        'Y', (2)
        'Manufacturer',
        '7',
        'Y',
        'N',
        NULL,
        NULL,
        'STRING',
        'N',
        NULL,
        NULL,
        NULL,
        'Y',
        '[{"queryType":"WORD","variantType":"STRING","boost":10}]',
        'N',
        100000,
        'N',
        '5DF1363059675161A85F576D');
  1. We need to make sure that this search field definition is translatable. This will ensure that the indexing process gathers translations for this field. For example, if the manufacturer DataDrivenEnum also has a Spanish translation for the display value, then the response from Solr query would look like this: Product’s Manufacturer with its Translations in Solr query response

  2. The attribute that sets the field to be faceted. This is only needed if you want the ability to facet products based on this field, which would look like this in the search results page: Manufacturer Facet in Search Results Page The search facet will also be translated based on current locale: Translated Manufacturer Facet in Search Results Page.

Lastly, we need to reference this script in the Liquibase master changelog, MyProject/services/search/src/main/resources/db/changelog/search.flexdemo.postgresql.changelog-master.yaml.

databaseChangeLog:
  - include:
      file: db/changelog/search.flexdemo.postgresql.changelog-master.xml
  # include any client change logs and additional Broadleaf upgrade change logs (drops etc...) below
  - include:
      file: create-manufacturer-field.sql

Product Consolidation

During the process of indexing, all of the products and their relations are consolidated into ConsolidatedProduct. This is primarily done via ProductConsolidationContributor implementations. DataDrivenEnumConsolidationContributor is responsible for contributing all DataDrivenEnum-related data.

Note
For more details on ConsolidatedProduct and ProductConsolidationContributor, you can learn more by referencing Catalog Search Integrations

To ensure that the manufacturer field’s data is available during indexing, we first need to extend the ConsolidatedProduct in CatalogServices, so that it uses the MyProduct, instead of Product as the projection domain.

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@EqualsAndHashCode(callSuper = true)
public class MyConsolidatedProduct extends ConsolidatedProduct {

    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;
    }

    @Getter
    @Setter
    private abstract static class ExcludedProductMethods {
        private Category primaryCategory;
    }
}

Next, we need to ensure that the manufacturer field is consolidated by the DataDrivenEnumConsolidationContributor.

public class MyDataDrivenEnumConsolidationContributor
        extends DataDrivenEnumConsolidationContributor {

    public MyDataDrivenEnumConsolidationContributor(
            TranslationEntityService<Translation> translationEntityService,
            DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService, TypeFactory typeFactory) {
        super(translationEntityService, dataDrivenEnumService, typeFactory);
    }

    @Override
    protected void addDataDrivenEnumIdsForProduct(ConsolidatedProduct product,
            List<String> enumIds) {
        super.addDataDrivenEnumIdsForProduct(product, enumIds);

        MyConsolidatedProduct myConsolidatedProduct = ((MyConsolidatedProduct) product);
        if (myConsolidatedProduct.getManufacturer() != null) {
            addDataDrivenEnumId(myConsolidatedProduct.getManufacturer(), enumIds);
        }
    }

    @Override
    protected void setDataDrivenEnumsForProduct(ConsolidatedProduct product,
            List<DataDrivenEnum> dataDrivenEnumsForAllProducts) {
        super.setDataDrivenEnumsForProduct(product, dataDrivenEnumsForAllProducts);

        MyConsolidatedProduct myConsolidatedProduct = ((MyConsolidatedProduct) product);
        setDataDrivenEnumIfPresent(myConsolidatedProduct, dataDrivenEnumsForAllProducts,
                MyConsolidatedProduct::getManufacturer, MyConsolidatedProduct::setManufacturer);
    }
}
Important
The extensions of ConsolidatedProduct and DataDrivenEnumConsolidationContributor should reside in CatalogServices, i.e. MyProject/services/catalog

Lastly, we need to register TypeSupplier beans for ConsolidatedProduct and MyDataDrivenEnumConsolidationContributor.

@Bean
@Primary
ProductConsolidationContributor myDataDrivenEnumConsolidationContributor(
        TranslationEntityService<Translation> translationEntityService,
        DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService,
        TypeFactory typeFactory) {
    return new MyDataDrivenEnumConsolidationContributor(translationEntityService,
            dataDrivenEnumService,
            typeFactory);
}

@Bean
public TypeSupplier myConsolidatedProductSupplier() {
    return () -> new TypeSupplier.TypeMapping(ConsolidatedProduct.class,
            MyConsolidatedProduct.class);
}

With these changes, the manufacturer field is ready to be indexed!

Solr Document Contributor

Now that the manufacturer field is indexed, we need to make sure that its translations are indexed as well.

To achieve this, we simply need to extend ProductTranslationSolrDocumentContributor in IndexerServices, i.e. MyProject/services/indexer.

public class MyProductTranslationSolrDocumentContributor
        extends ProductTranslationSolrDocumentContributor {

    public MyProductTranslationSolrDocumentContributor(SolrFieldService solrFieldService,
            DocumentBuilderHelper documentBuilderHelper, TypeFactory typeFactory,
            IndexerTenantService tenantService) {
        super(solrFieldService, documentBuilderHelper, typeFactory, tenantService);
    }

    @Override
    protected List<String> getDataDrivenEnumJsonPaths(@lombok.NonNull Translation translation) {
        List<String> jsonPaths = new ArrayList<>(super.getDataDrivenEnumJsonPaths(translation)); (1)

        String manufacturerPath =
                transformToJsonTranslationPath(translation,
                        String.format("manufacturer[?(@.id == \"%s\")]",
                                translation.getEntityId()));
        jsonPaths.add(manufacturerPath);

        return jsonPaths;
    }
}
  1. Make sure that the json paths from the overridden method are added into a new ArrayList, so that we can add the json path for manufacturer to the list (the List returned from the overridden method is an unmodifiable List).

Next, we need to define the configuration class to register our bean.

@Configuration
public class MySolrCatalogDocumentBuilderAutoConfiguration {
    @Bean
    @Primary
    public DocumentBuilderContributor<SolrInputDocument> myProductTranslationSolrDocumentContributor(
            SolrFieldService solrFieldService,
            DocumentBuilderHelper documentBuilderHelper,
            TypeFactory typeFactory,
            IndexerTenantService tenantService) {
        return new MyProductTranslationSolrDocumentContributor(solrFieldService,
                documentBuilderHelper,
                typeFactory,
                tenantService);
    }
}

Lastly, we need to add the configuration class to the META-INF/spring.factories file.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.broadleafdemo.indexer.autoconfigure.MySolrCatalogDocumentBuilderAutoConfiguration
Important
The extension of ProductTranslationSolrDocumentContributor and other changes should reside in IndexerServices, i.e. MyProject/services/indexer

That’s it! The manufacturer field and its translations are now ready to be indexed!

Adding the New Field to ProductDetails

To display the manufacturer field in the Product Details Page, or to apply specific offers or price lists based on the manufacturer (tutorial coming soon), we will need to customize ProductDetails.

First, we need to extend ProductDetails to use our MyProduct projection domain in CatalogServices, i.e. MyProject/services/catalog.

@Data
@NoArgsConstructor
@AllArgsConstructor
@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
    @AllArgsConstructor
    private abstract static class ExcludedProductMethods {
        private List<IncludedProduct> includedProducts;

        private List<ProductOption> options;

        public abstract String getMetaDescription();

        public abstract String getMetaTitle();
    }
}

Next, we need to extend RelatedDataDrivenEnumsProductContextContributor, which is responsible for contributing all of the related DataDrivenEnums for products.

public class MyRelatedDataDrivenEnumsProductContextContributor
        extends RelatedDataDrivenEnumsProductContextContributor {

    public MyRelatedDataDrivenEnumsProductContextContributor(
            DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService) {
        super(dataDrivenEnumService);
    }

    @Override
    protected void addDataDrivenEnumIdsForProduct(@lombok.NonNull Product product,
            @lombok.NonNull Map<String, Set<String>> enumIdsByProductId) {
        super.addDataDrivenEnumIdsForProduct(product, enumIdsByProductId);
        addDataDrivenEnumIdByProductId(((MyProduct) product).getManufacturer(), product.getId(),
                enumIdsByProductId);
    }
}

Then, we need to extend DataDrivenEnumsProductDetailsContributor, which is responsible for populating the corresponding fields.

public class MyDataDrivenEnumsProductDetailsContributor
        extends DataDrivenEnumsProductDetailsContributor {

    @Override
    protected void setDataDrivenEnums(@lombok.NonNull ProductDetails productDetails,
            @lombok.NonNull List<DataDrivenEnum> relatedDataDrivenEnums) {
        super.setDataDrivenEnums(productDetails, relatedDataDrivenEnums);

        MyProductDetails myProductDetails = (MyProductDetails) productDetails;
        setDataDrivenEnumIfPresent(relatedDataDrivenEnums, myProductDetails,
                MyProductDetails::getManufacturer,
                MyProductDetails::setManufacturer);
    }
}

Lastly, we need to add the TypeSupplier and contributor beans to our MyCatalogServicesAutoConfiguration.

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

@Bean
@Primary
public ProductDetailsContextContributor myRelatedDataDrivenEnumsProductContextContributor(
        DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService) {
    return new MyRelatedDataDrivenEnumsProductContextContributor(dataDrivenEnumService);
}

@Bean
@Primary
ProductDetailsContributor myDataDrivenEnumsProductDetailsContributor() {
    return new MyDataDrivenEnumsProductDetailsContributor();
}
Note
For more details about ProductDetails and contributors, you can learn more by referencing Facilitating Product Details Pages

With all the changes above, your MyCatalogServicesAutoConfiguration should look like this:

@Configuration
public class MyCatalogServicesAutoConfiguration {

    @Bean
    @Primary
    public ProductHydrationService myProductHydrationService(
            ProductService<Product> productService,
            ProductAssetService<ProductAsset> productAssetService,
            VariantService<Variant> variantService,
            CategoryService<Category> categoryService,
            CategoryProductService<CategoryProduct> categoryProductService,
            DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService,
            TypeFactory typeFactory) {
        return new MyProductHydrationService(
                productService,
                productAssetService,
                variantService,
                categoryService,
                categoryProductService,
                dataDrivenEnumService,
                typeFactory);
    }

    @Bean
    @Primary
    ProductConsolidationContributor myDataDrivenEnumConsolidationContributor(
            TranslationEntityService<Translation> translationEntityService,
            DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService,
            TypeFactory typeFactory) {
        return new MyDataDrivenEnumConsolidationContributor(translationEntityService,
                dataDrivenEnumService,
                typeFactory);
    }

    @Bean
    @Primary
    public ProductDetailsContextContributor myRelatedDataDrivenEnumsProductContextContributor(
            DataDrivenEnumService<DataDrivenEnum> dataDrivenEnumService) {
        return new MyRelatedDataDrivenEnumsProductContextContributor(dataDrivenEnumService);
    }

    @Bean
    @Primary
    ProductDetailsContributor myDataDrivenEnumsProductDetailsContributor() {
        return new MyDataDrivenEnumsProductDetailsContributor();
    }

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

    @Bean
    public TypeSupplier myConsolidatedProductSupplier() {
        return () -> new TypeSupplier.TypeMapping(ConsolidatedProduct.class,
                MyConsolidatedProduct.class);
    }

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

That’s it! Now our new manufacturer DataDrivenEnum field will be part of the ProductDetails! If you inspect the ProductDetails payload in a product details page, you can see:

Manufacturer in Product Details