Broadleaf Microservices
  • v1.0.0-latest-prod

Data Driven Enums

As of Broadleaf 1.5, a new domain DataDrivenEnum was introduced to dynamically manage enums via the admin. This is particularly useful for fields that only allow certain values while having the ability to be modified without any re-deployments.

For example, if the Product domain had a field to represent its material, DataDrivenEnums can be directly mapped to this field based on its type instead of relying on metadata-based enums, so that the enums can be managed dynamically without needing a re-deployment.

Data Driven Enums in Admin

DataDrivenEnums can be managed in the admin under the Catalog navigation menu:

DataDrivenEnum Admin Menu Item

You can see all the defined enums in the same grid:

DataDrivenEnum Admin Grid

You can create enums with different types, values, and display values:

Creating DataDrivenEnum

The display value of the DataDrivenEnum is also translatable, so that if there’s any need to be used in commerce, it would be correctly translated based on current locale. For example, faceting products based on a DataDrivenEnum field such as material.

DataDrivenEnum Admin Grid in Spanish

Out-of-Box Data Driven Enums

From out-of-box, the Product domain has three DataDrivenEnum fields, brand, merchandising type, and target demographic, and these fields can be used to further categorize products.

Important
The metadata for these fields are not added by default. To utilize these fields, you will need to manually add their metadata in your project by following the instructions in the next section.

Adding Metadata for Out-of-Box Data Driven Enums

To add the metadata for the out-of-box DataDrivenEnum fields, we need to create some configuration classes in the metadata services (i.e. MyProject/services/metadata).

First, a my-catalog-messages.properties messages file needs to be created for the metadata field labels:

# Fields
## Product Fields
product.fields.brand=Brand
product.fields.merchandisingType=Merchandising Type
product.fields.targetDemographic=Target Demographic

Then, that messages file needs to be registered:

public class MyCustomMetadataMessages implements MetadataMessagesBasename {

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

Once the labels are ready, a metadata autoconfiguration class needs to be created:

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(CatalogMetadataProperties.class)
@AutoConfigureAfter(CatalogServicesMetadataAutoConfiguration.class)
public class MyCatalogMetadataAutoConfiguration {

    private final CatalogMetadataProperties properties; // (1)

    @UtilityClass
    public class DemoProductProps {
        public final String BRAND = "brand";
        public final String MERCHANDISING_TYPE = "merchandisingType";
        public final String TARGET_DEMOGRAPHIC = "targetDemographic";
        public final String BRAND_ID = BRAND + "Id";
        public final String MERCHANDISING_TYPE_ID = MERCHANDISING_TYPE + "Id";
        public final String TARGET_DEMOGRAPHIC_ID = TARGET_DEMOGRAPHIC + "Id";
    }

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

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

                merchandisingGroup
                        .addField(DemoProductProps.BRAND,
                                createDataDrivenEnumLookup(DataDrivenEnumTypes.BRAND, // (3)
                                        "product.fields.brand") // (4)
                                                .order(1100))
                        .addField(DemoProductProps.MERCHANDISING_TYPE,
                                createDataDrivenEnumLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
                                        "product.fields.merchandisingType")
                                                .order(1200))
                        .addField(DemoProductProps.TARGET_DEMOGRAPHIC,
                                createDataDrivenEnumLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
                                        "product.fields.targetDemographic")
                                                .order(1300));
            }
        };
    }

    /**
     * Retrieve the list of active product types.
     *
     * @return a list of active product types
     */
    protected List<DefaultProductType> getAvailableProductTypes() { // (5)
        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, in order to add the DataDrivenEnum fields metadata to the forms across all types.

  2. This bean adds the DataDrivenEnum fields metadata to the product’s update form in the "Merchandising" group, using DataDrivenEnumLookupHelpers to lookup DataDrivenEnums by id.

  3. Both createDataDrivenEnumLookup and DataDrivenEnumTypes are defined in DataDrivenEnumLookupHelpers.

  4. Labels defined earlier in the my-catalog-messages.properties messages file.

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

Lastly, a META-INF/spring.factories file needs to be created for the messages and our MetadataAutoConfiguration:

com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
  com.myproject.metadata.i18n.MyCustomMetadataMessages

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.myproject.metadata.catalog.MyCatalogMetadataAutoConfiguration

Updating Rule Builders to Target the Out-of-Box Data Driven Enum Fields

Category Rule Builders

For rule-based categories, you can define product membership rules to include products in the category based on certain Product fields (i.e. Product’s SKU, name, UPC, etc). If you want to add the out-of-box data driven enum fields to the product membership rule builder, you can simply add the following bean to the MyCatalogMetadataAutoConfiguration that we defined earlier.

@Bean
public ComponentSource addDataDrivenEnumFieldsToCategoryProductMembershipRule() {
    return registry -> {

        TreeView<?> categoryTree = (TreeView<?>) registry.get(CategoryIds.TREE);
        FormView<?> categoryProductUpdate =
                categoryTree.getUpdateForm(CategoryIds.Forms.PRODUCTS);

        Group<?> productMembershipRuleGroup = categoryProductUpdate
                .getGroup(CategoryGroups.PRODUCT_MEMBERSHIP_RULE);

        QueryBuilderField<?> ruleBuilder = (QueryBuilderField<?>) productMembershipRuleGroup
                .getField(CategoryProps.PRODUCT_MEMBERSHIP_RULE);

        ruleBuilder
                .addField(DemoProductProps.BRAND_ID,
                        createDataDrivenEnumIdLookup(DataDrivenEnumTypes.BRAND,
                                "product.fields.brand")
                                        .order(7000))
                .addField(DemoProductProps.MERCHANDISING_TYPE_ID,
                        createDataDrivenEnumIdLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
                                "product.fields.merchandisingType")
                                        .order(8000))
                .addField(DemoProductProps.TARGET_DEMOGRAPHIC_ID,
                        createDataDrivenEnumIdLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
                                "product.fields.targetDemographic")
                                        .order(9000));
    };
}

Offer Rule Builders

Similarly, there are also rule builders that you can modify to target the out-of-box data driven enums in the item qualifier and target item criteria in Offer. To do so, we can follow the similar steps above in the metadata services (i.e. MyProject/services/metadata), but this time, instead of making the changes in com.myproject.metadata.catalog, we will create a new package com.myproject.metadata.offer.

Note
If unfamiliar with Offers, you can learn more by referencing OfferServices Documentation

First, a my-offer-messages.properties messages file needs to be created for the metadata field labels:

# Fields
## Offer Fields
offer.fields.item-criteria.brand=Item Brand
offer.fields.item-criteria.merchandising-type=Item Merchandising Type
offer.fields.item-criteria.target-demographic=Item Target Demographic

Then, that messages file needs to be registered in the existing MetadataMessagesBasename implementation that we created earlier:

public class MyCustomMetadataMessages implements MetadataMessagesBasename {

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

Once the labels are ready, a metadata autoconfiguration class needs to be created:

@Configuration
@RequiredArgsConstructor
@AutoConfigureAfter(OfferServicesMetadataAutoConfiguration.class)
public class MyOfferMetadataAutoConfiguration {

    @UtilityClass
    public class DemoOfferProps { // (1)
        public final String ITEM_BRAND = "attributes[brandId]";
        public final String ITEM_MERCHANDISING_TYPE = "attributes[merchandisingTypeId]";
        public final String ITEM_TARGET_DEMOGRAPHIC = "attributes[targetDemographicId]";
    }

    @Bean
    public ComponentSource addDataDrivenEnumFieldsToOfferCriteriaRuleBuilder() {
        return registry -> {
            List<FieldArrayBlockField<?>> criteriaFields = getOfferCriteriaFields(
                    (EntityView<?>) registry.get(OfferIds.CREATE),
                    (EntityView<?>) registry.get(OfferIds.UPDATE));
            List<RuleBuilderField<?>> ruleBuilders = getRuleBuilderFields(criteriaFields);

            ruleBuilders.forEach(ruleBuilder -> ruleBuilder
                    .addField(DemoOfferProps.ITEM_BRAND,
                            createDataDrivenEnumIdLookup(DataDrivenEnumTypes.BRAND, // (2)
                                    "offer.fields.item-criteria.brand")
                                            .order(7100))
                    .addField(DemoOfferProps.ITEM_MERCHANDISING_TYPE,
                            createDataDrivenEnumIdLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
                                    "offer.fields.item-criteria.merchandising-type")
                                            .order(7200))
                    .addField(DemoOfferProps.ITEM_TARGET_DEMOGRAPHIC,
                            createDataDrivenEnumIdLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
                                    "offer.fields.item-criteria.target-demographic")
                                            .order(7300)));
        };
    }

    protected List<FieldArrayBlockField<?>> getOfferCriteriaFields(EntityView<?>... entityViews) {
        List<FieldArrayBlockField<?>> criteriaFields = new ArrayList<>();

        for (EntityView<?> view : entityViews) {
            FormView<?> generalForm = view.getGeneralForm();
            Group<?> itemQualifierGroup =
                    generalForm.getGroup(OfferGroupIds.OFFER_ITEM_QUALIFIERS_GROUP);
            Group<?> targetItemGroup =
                    generalForm.getGroup(OfferGroupIds.OFFER_TARGET_ITEMS_FIELD_GROUP);
            // (3)
            FieldArrayBlockField<?> itemQualifierCriteria =
                    (FieldArrayBlockField<?>) itemQualifierGroup
                            .getField(OfferProps.ITEM_QUALIFIER_CRITERIA);
            FieldArrayBlockField<?> targetItemCriteria = (FieldArrayBlockField<?>) targetItemGroup
                    .getField(OfferProps.TARGET_ITEM_CRITERIA);
            criteriaFields.add(itemQualifierCriteria);
            criteriaFields.add(targetItemCriteria);
        }

        return criteriaFields;
    }

    protected List<RuleBuilderField<?>> getRuleBuilderFields(
            List<FieldArrayBlockField<?>> criteriaFields) {
        List<RuleBuilderField<?>> ruleBuilders = new ArrayList<>();

        criteriaFields.forEach(criteriaField -> {
            RuleBuilderField<?> fulfillmentGroup =
                    (RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
                            .getFieldKey(OfferProps.ItemCriteriaRule.FULFILLMENT_GROUP_RULES));
            RuleBuilderField<?> fulfillmentItem =
                    (RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
                            .getFieldKey(OfferProps.ItemCriteriaRule.FULFILLMENT_ITEM_RULES));
            RuleBuilderField<?> order =
                    (RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
                            .getFieldKey(OfferProps.ItemCriteriaRule.ORDER_RULES));
            RuleBuilderField<?> orderItem =
                    (RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
                            .getFieldKey(OfferProps.ItemCriteriaRule.ORDER_ITEM_RULES));

            // (4)
            ruleBuilders.add(fulfillmentGroup);
            ruleBuilders.add(fulfillmentItem);
            ruleBuilders.add(order);
            ruleBuilders.add(orderItem);
        });

        return ruleBuilders;
    }
}
  1. Defines where those data driven enum IDs are located in OrderLineItemDto, they are added in OrderLineItemDto#attributes by default.

  2. Both createDataDrivenEnumLookup and DataDrivenEnumTypes are defined in DataDrivenEnumLookupHelpers.

  3. Gets the criteria fields from both item qualifier and target item criteria groups.

  4. Since we use the same fields in the rule builders for fulfillment group, fulfillment item, order, and order item, we need to make sure to add the out-of-box data driven enum fields to all the rule builders. This is not necessary if not needed for your use cases, however.

Lastly, we need to add our MetadataAutoConfiguration to META-INF/spring.factories that we created earlier:

com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
  com.myproject.metadata.i18n.MyCustomMetadataMessages

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.myproject.metadata.catalog.MyCatalogMetadataAutoConfiguration,\
  com.myproject.metadata.offer.MyOfferMetadataAutoConfiguration

Then you’re done! You can now build rules with those fields:

Out-of-Box Data Driven Enum Fields in Offer Rule Builder

PriceList Rule Builders

In PriceList, there is also a rule builder in the price modifier, which only has target type and id by default (referring to PriceableTarget’s type and id). To target the out-of-box data driven enums in the price modifier in PriceList, we will create a new package `com.myproject.metadata.pricing.

Note
If unfamiliar with PriceLists, you can learn more by referencing PricingServices Documentation

First, a my-pricing-messages.properties messages file needs to be created for the metadata field labels:

# Fields
## Price List Fields
price-list.fields.item-criteria.brand=Item Brand
price-list.fields.item-criteria.merchandising-type=Item Merchandising Type
price-list.fields.item-criteria.target-demographic=Item Target Demographic
Note
You can also create a common messages file and define common labels there

Then, that messages file needs to be registered in the existing MetadataMessagesBasename implementation that we created earlier:

public class MyCustomMetadataMessages implements MetadataMessagesBasename {

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

Once the labels are ready, a metadata autoconfiguration class needs to be created:

@Configuration
@RequiredArgsConstructor
@AutoConfigureAfter(PricingServicesMetadataAutoConfiguration.class)
public class MyPricingMetadataAutoConfiguration {

    @UtilityClass
    public class DemoPriceListProps { // (1)
        public final String ITEM_BRAND = "attributes[skuRef]?.brand?.id";
        public final String ITEM_MERCHANDISING_TYPE = "attributes[skuRef]?.merchandisingType?.id";
        public final String ITEM_TARGET_DEMOGRAPHIC = "attributes[skuRef]?.targetDemographic?.id";
    }

    @Bean
    public ComponentSource addDataDrivenEnumFieldsToPriceListPriceModifierRuleBuilder() {
        return registry -> {
            List<EntityView<?>> entityViews = new ArrayList<>(); // (2)
            entityViews.add((EntityView<?>) registry.get(PricingIds.CREATE));
            entityViews.add((EntityView<?>) registry.get(PricingIds.UPDATE));
            entityViews.add((EntityView<?>) registry.get(PricingIds.SALE_CREATE));
            entityViews.add((EntityView<?>) registry.get(PricingIds.SALE_UPDATE));
            entityViews.add((EntityView<?>) registry.get(PricingIds.CONTRACT_CREATE));
            entityViews.add((EntityView<?>) registry.get(PricingIds.CONTRACT_UPDATE));

            entityViews.forEach(view -> {
                FormView<?> generalForm = view.getGeneralForm();
                Group<?> priceModifierGroup = generalForm.getGroup(PricingGroups.PRICE_MODIFIER);
                Group<?> priceModifierSubGroup =
                        priceModifierGroup.getGroup(PricingGroups.PRICE_MODIFIER_SUB);

                RuleBuilderField<?> priceModifierCriteriaRuleBuilder =
                        (RuleBuilderField<?>) priceModifierSubGroup
                                .getField(PricingProps.PriceModifier.PRICEMODIFIER_CRITERIA);
                priceModifierCriteriaRuleBuilder
                        .addField(DemoPriceListProps.ITEM_BRAND,
                                createDataDrivenEnumIdLookup(DataDrivenEnumTypes.BRAND, // (3)
                                        "price-list.fields.item-criteria.brand")
                                                .order(3000))
                        .addField(DemoPriceListProps.ITEM_MERCHANDISING_TYPE,
                                createDataDrivenEnumIdLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
                                        "price-list.fields.item-criteria.merchandising-type")
                                                .order(4000))
                        .addField(DemoPriceListProps.ITEM_TARGET_DEMOGRAPHIC,
                                createDataDrivenEnumIdLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
                                        "price-list.fields.item-criteria.target-demographic")
                                                .order(5000));
            });
        };
    }
}
  1. Defines where those data driven enum IDs are located in PriceableTarget, they are added in skuRef in PriceableTarget#attributes by default.

  2. Since there are different types of price lists (normal price lists, sales, and contracts) and the price modifier is in both create and update forms, we need to make sure we modify the metadata for all those EntityViews.

  3. Both createDataDrivenEnumLookup and DataDrivenEnumTypes are defined in DataDrivenEnumLookupHelpers.

Lastly, we need to add our MetadataAutoConfiguration to META-INF/spring.factories that we created earlier:

com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
  com.myproject.metadata.i18n.MyCustomMetadataMessages

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.myproject.metadata.catalog.MyCatalogMetadataAutoConfiguration,\
  com.myproject.metadata.offer.MyOfferMetadataAutoConfiguration,\
  com.myproject.metadata.offer.MyPricingMetadataAutoConfiguration

Then you’re done! You can now build rules with those fields:

Out-of-Box Data Driven Enum Fields in PriceList Rule Builder