Develop a New Commerce Connector

This Bloomreach Experience Manager feature requires a standard or premium license. Please contact Bloomreach for more information.

Introduction

Goal

Learn how to develop and test a commerce connector module.

Overview

In this tutorial, you will create a basic commerce connector module, for learning and demonstration purpose, which reads product and category data from a static JSON resource file and performs basic operations for customer and cart managements with simple in-memory data storages.

In addition to implementation code, you will also write unit test code for a better quality and self-validation at module level.

Finally, you will learn how to perform an integration test with the commerce connector module by dropping and registering the module in Bloomreach B2C Commerce Accelerator project or Bloomreach B2B Commerce Accelerator project.

Source of Demo Commerce Connector Module Project

You can find all source files listed on this page in the Demo Commerce Connector Module project located at https://github.com/onehippo/demo-commerce-connector. You may clone, build, and test the examples as follows:

$ git clone https://github.com/onehippo/demo-commerce-connector.git
$ cd demo-commerce-connector
$ mvn clean install

The last command will compile, perform unit tests, package the JAR module, and install the module to your local Maven repository.

Structure of Commerce Connector Module Project

A Commerce Connector Module must be packaged in a JAR module as an HST Addon Module, so that Bloomreach Commerce Accelerator Application may discover the module and recognize its CommerceRepository components. An HST Addon Module must define its module and Spring Bean Assembly locations through the module descriptor at META-INF/hst-assembly/addon/module.xml.

The project contains two submodules: one is B2C Demo Commerce Connector Module in b2c folder, and the other is B2B Demo Commerce Connector Module in b2b folder.

Module Descriptor

You should configure a unique name for your Commerce Connector Module. It should be reverse-DNS, reflecting your organization and module structure, like com.foo.connectors.foocommerce or com.bloomreach.commercedxp.demo.connectors.mydemoconnector. Bloomreach Commerce Accelerator will be able to discover your Commerce Connector Module by its name.

In the example below, com.bloomreach.commercedxp.demo.connectors.mydemoconnector is the unique module name for the B2C Demo Commerce Connector Module.

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://www.onehippo.org/schema/hst/hst-addon-module_1_0.xsd">
  <name>com.bloomreach.commercedxp.demo.connectors.mydemoconnector</name>
  <config-locations>
    <config-location>classpath*:META-INF/spring-assembly/addon/com/bloomreach/commercedxp/demo/connectors/mydemoconnector/*.xml</config-location>
    <config-location>classpath*:META-INF/hst-assembly/addon/com/bloomreach/commercedxp/demo/connectors/mydemoconnector/overrides/*.xml</config-location>
  </config-locations>
</module>

Also, com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector is the unique module name for the B2B Demo Commerce Connector Module.

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://www.onehippo.org/schema/hst/hst-addon-module_1_0.xsd">
  <name>com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector</name>
  <config-locations>
    <config-location>classpath*:META-INF/spring-assembly/addon/com/bloomreach/commercedxp/demo/connectors/myb2bdemoconnector/connector-*.xml</config-location>
    <config-location>classpath*:META-INF/hst-assembly/addon/com/bloomreach/commercedxp/demo/connectors/myb2bdemoconnector/overrides/connector-*.xml</config-location>
  </config-locations>
</module>

An HST Addon Module allows to assemble any component beans of your Commerce Connector Module in Spring Bean Assembly XML files through the <config-locations> and <config-location> elements.

CommerceRepository Beans Assembly

Here is an example Spring Bean Assembly file in the B2C Demo Commerce Connector Module:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <!--
    The entrypoint component bean, of type c.b.c.api.v2.connector.provider.ConnectorRepositoryProvider,
    through which Bloomreach Commerce Accelerator retrieves your CommerceRepository components.
  -->
  <bean class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoConnectorRepositoryProviderImpl">
    <property name="commerceRepositoryMap">
      <map>
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.CategoryRepository"
               value-ref="categoryRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.ProductRepository"
               value-ref="productRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.CustomerRepository"
               value-ref="customerRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.AddressRepository"
               value-ref="addressRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.CartRepository"
               value-ref="cartRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.OrderRepository"
               value-ref="orderRepository" />
      </map>
    </property>
  </bean>

  <bean id="categoryRepository" class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoCategoryRepositoryImpl">
  </bean>

  <bean id="productRepository" class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoProductRepositoryImpl">
  </bean>

  <bean id="customerRepository" class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoCustomerRepositoryImpl">
  </bean>

  <bean id="cartRepository" class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoCartRepositoryImpl">
  </bean>

  <bean id="orderRepository" class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoOrderRepositoryImpl">
  </bean>

  <bean id="addressRepository" class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoAddressRepositoryImpl">
  </bean>

</beans>

Also, here is an example Spring Bean Assembly file in the B2B Demo Commerce Connector Module:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <!--
    The entrypoint component bean, of type c.b.c.api.v2.connector.provider.ConnectorRepositoryProvider,
    through which StarterStore retrieves your CommerceRepository components.
  -->
  <bean class="com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository.MyDemoConnectorRepositoryProviderImpl">
    <property name="commerceRepositoryMap">
      <map>
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.CategoryRepository"
               value-ref="bizCategoryRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizCategoryRepository"
               value-ref="bizCategoryRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.ProductRepository"
               value-ref="bizProductRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizProductRepository"
               value-ref="bizProductRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.AccountRepository"
               value-ref="bizAccountRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.CustomerRepository"
               value-ref="bizCustomerRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizCustomerRepository"
               value-ref="bizCustomerRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.AddressRepository"
               value-ref="bizAddressRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizAddressRepository"
               value-ref="bizAddressRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.CartRepository"
               value-ref="bizCartRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizCartRepository"
               value-ref="bizCartRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.OrderRepository"
               value-ref="bizOrderRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizOrderRepository"
               value-ref="bizOrderRepository" />
        <entry key="com.bloomreach.commercedxp.api.v2.connector.repository.WishListRepository"
               value-ref="bizWishListRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizWishListRepository"
               value-ref="bizWishListRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizStoredPaymentRepository"
               value-ref="bizStoredPaymentRepository" />
        <entry key="com.bloomreach.commercedxp.b2b.api.v2.connector.repository.BizInvoiceRepository"
               value-ref="bizInvoiceRepository" />
      </map>
    </property>
  </bean>

  <bean id="bizCategoryRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizCategoryRepositoryImpl">
  </bean>

  <bean id="bizProductRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizProductRepositoryImpl">
  </bean>

  <bean id="bizAccountRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizAccountRepositoryImpl">
  </bean>

  <bean id="bizCustomerRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizCustomerRepositoryImpl">
    <property name="accountRepository" ref="bizAccountRepository" />
  </bean>

  <bean id="bizAddressRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizAddressRepositoryImpl">
  </bean>

  <bean id="bizCartRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizCartRepositoryImpl">
  </bean>

  <bean id="bizOrderRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizOrderRepositoryImpl">
  </bean>

  <bean id="bizWishListRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizWishListRepositoryImpl">
  </bean>

  <bean id="bizStoredPaymentRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizStoredPaymentRepositoryImpl">
  </bean>

  <bean id="bizInvoiceRepository" class="com.bloomreach.commercedxp.demo.connectors.myb2bdemoconnector.repository.MyDemoBizInvoiceRepositoryImpl">
  </bean>

</beans>

After discovering the Commerce Connector Module as an HST Addon Module, Bloomreach Commerce Accelerator Application retrieves the component bean, of type com.bloomreach.commercedxp.api.v2.connector.provider.ConnectorRepositoryProvider, and retrieves all the available CommerceRepository components through it.

A Commerce Connector Module is responsible for the following:

  • It should be packaged in a JAR file as an HST Addon Module, and provide the entry point bean of type com.bloomreach.commercedxp.api.v2.connector.provider.ConnectorRepositoryProvider in its Spring Bean Assembly XML file, through which Bloomreach Commerce Accelerator Application can have access to the module.
  • The ConnectorRepositoryProvider bean is responsible for providing all the available CommerceRepository beans. Applications depends on the ConnectorRepositoryProvider bean to retrieve CommerceRepository components, not trying to retrieve the beans directly.

You can implement a more complex ConnectorRepositoryProvider class if necessary, or you can extend AbstractConnectorRepositoryProvider that provides basic getter and setter methods for the CommerceRepository component beans like the following:

package com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository;

import com.bloomreach.commercedxp.api.v2.connector.provider.AbstractConnectorRepositoryProvider;

/**
 * Simple ConnectorRepositoryProvider extending AbstractConnectorRepositoryProvider to just set/get CommerceRepository beans.
 * It inherits all the getters and setters for the CommerceRepository beans from AbstractConnectorRepositoryProvider.
 */
public class MyDemoConnectorRepositoryProviderImpl extends AbstractConnectorRepositoryProvider {

}

Implement and Test CommerceRepository

In this tutorial, you'll use a static JSON resource file for the product and category data without having to interact with any real backends at runtime, just for simplicity. The example static JSON resource file is located at src/main/resources/com/bloomreach/commercedxp/demo/connectors/mydemoconnector/demoproducts.json in the project. It looks as follows:

{
  "response":{
    "numFound":10,
    "start":0,
    "docs":[
      {
        "sale_price":49.99,
        "price":49.99,
        "description":"Vestri M-Class logo anchors the signature web stripes racing around the collar of a women's polo cut for comfort from knit stretch cotton.",
        "title":"Women's M-Class Tee",
        "url":"www.elasticpath.com",
        "brand":"",
        "pid":"WOMENS_M-Class_TEE",
        "default_sku": "97115",
        "categories": [
          "VPA_T_MCLASS"
        ],
        "thumb_image":"https://s3-us-west-2.amazonaws.com/elasticpath-demo-images/VESTRI_VIRTUAL/97115.png",
        "sale_price_range":[
          49.99,
          49.99
        ],
        "price_range":[
          49.99,
          49.99
        ]
      },
      {
        "sale_price":35900.0,
        "price":35900.0,
        "description":"Our full size electric/hybrid primum four door sedan gives you the performance of a sports car with the function of a sedan. Perfect for family needing something bigger, but still wanting to feel young at heart. The performance and reliability is such that it's been selected as the de factor electric police car by many governments. It's extended range, options give everything needed for the environmentally conscious service.",
        "title":"X-Class Full Size Premium Sedan",
        "url":"www.elasticpath.com",
        "brand":"",
        "pid":"X-Class-S",
        "default_sku": "10002",
        "categories": [
          "VPA_T_MCLASS"
        ],
        "thumb_image":"https://s3-us-west-2.amazonaws.com/elasticpath-demo-images/VESTRI_VIRTUAL/10002.png",
        "sale_price_range":[
          35900.0,
          35900.0
        ],
        "price_range":[
          35900.0,
          35900.0
        ]
      },
      // SNIP
    ]
  },
  "category_map":{
    "VESTRI_BM_APPAREL":"Apparel",
    "VPA_CHARGING_AND_ADAPTERS":"Charging and Adapters",
    "VPA_TA_T50":"T50",
    "VPA_VA_MCLASS":"M-Class",
    "VPA_VA_T50":"T50",
    "VPA_CA_XCLASS":"X-Class",
    "VPA_T_MCLASS":"M-Class",
    "VESTRI_BM_ACCESSORIES":"Accessories",
    "VESTRI_APPAREL_WOMENS":"Womens",
    "VPA_CA_MCLASS":"M-Class",
    "VPA_T_T50":"T50",
    "VESTRI_APPAREL_MENS":"Mens",
    "VPA_CHARING_AND_ADAPTERS":"Charging and Adapters",
    "VPA_TIRES":"Tires",
    "VPA_VEHICLE_ADDONS":"Addons",
    "VPA_T_XCLASS":"X-Class"
  }
}

The JSON file contains all product item resources in an array in the response/docs node, and all the navigational category map in the category_map node. So, the demo CommerceRepository implementations will be able to provide product and category data based on the static JSON resource, using the following utility class:

package com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository;

// SNIP

/**
 * Demo Data Loader Utility.
 */
public class MyDemoDataLoader {

    /**
     * The name of the optional system property which specifies demo products data in CSV format.
     */
    public static final String SYS_PROP_DEMO_PRODUCTS_CSV = "demo.products.csv";

    /**
     * The name of the optional system property which specifies demo categories data in CSV format.
     */
    public static final String SYS_PROP_DEMO_CATEGORIES_CSV = "demo.categories.csv";

    /**
     * Default demo product data JSON resource path in BrSM product response data format.
     */
    private static final String DEFAULT_PRODUCT_DATA_JSON_RESOURCE = "com/bloomreach/commercedxp/demo/connectors/mydemoconnector/demoproducts.json";

    private static MyDemoData loadMyDemoData() {
        MyDemoData demoData = null;

        // First, load product/catalog data from the default built-in json file.
        try (InputStream input = MyDemoDataLoader.class.getClassLoader()
                .getResourceAsStream(DEFAULT_PRODUCT_DATA_JSON_RESOURCE)) {
            final ObjectMapper objectMapper = new ObjectMapper();
            demoData = objectMapper.readValue(input, MyDemoData.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (demoData != null) {
            // If custom product data file is provided in csv format, replace product data by the data in the CSV.
            String productsCsvProp = System.getProperty(SYS_PROP_DEMO_PRODUCTS_CSV);

            if (StringUtils.isNotBlank(productsCsvProp)) {
                productsCsvProp = StrSubstitutor.replaceSystemProperties(productsCsvProp);
                final URL productsCsvUrl = MyDemoDataResourceUtils.getResource(productsCsvProp);

                if (productsCsvUrl != null) {
                    try (InputStream input = productsCsvUrl.openStream()) {
                        final CsvSchema csvSchema = CsvSchema.emptySchema().withHeader();
                        final ObjectMapper csvMapper = new CsvMapper();
                        final MappingIterator<MyDemoProductItem> mappingIt = csvMapper
                                .readerFor(MyDemoProductItem.class).with(csvSchema).readValues(input);
                        demoData.getResponse().setProductItems(mappingIt.readAll());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            // If custom category data file is provided in csv format, replace category data by the data in the CSV.
            String categoriesCsvProp = System.getProperty(SYS_PROP_DEMO_CATEGORIES_CSV);

            if (StringUtils.isNotBlank(categoriesCsvProp)) {
                categoriesCsvProp = StrSubstitutor.replaceSystemProperties(categoriesCsvProp);
                final URL categoriesCsvUrl = MyDemoDataResourceUtils.getResource(categoriesCsvProp);

                if (categoriesCsvUrl != null) {
                    try (InputStream input = categoriesCsvUrl.openStream()) {
                        final CsvSchema csvSchema = CsvSchema.emptySchema().withHeader();
                        final ObjectMapper csvMapper = new CsvMapper();
                        final MappingIterator<MyDemoCategoryModel> mappingIt = csvMapper
                                .readerFor(MyDemoCategoryModel.class).with(csvSchema).readValues(input);
                        demoData.setCategoryModels(mappingIt.readAll());
                        demoData.resetCategoryModelHierarchy();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return demoData;
    }

    static MyDemoData myDemoData;

    protected MyDemoDataLoader() {
    }

    /**
     * Return the static MyDemoData instance which is loaded from the static JSON resource.
     */
    public static synchronized MyDemoData getMyDemoData() {
        MyDemoData data = myDemoData;

        if (data == null) {
            data = loadMyDemoData();
            myDemoData = data;
        }

        return data;
    }

    /**
     * Clear the existing static MyDemoData instance. e.g, from unit test to reset the data.
     */
    protected static synchronized void clearMyDemoData() {
        myDemoData = null;
    }
}

CommerceRepository implementations will be able to use MyDemoDataLoader#getMyDemoData() whenever they need to access the internal data.

Implement ProductRepository

Start with implementing ProductRepository. Implement #findOne(...) to retrieve the specific one product item, #findAll(...) to query product items, and #findAllByCategory(...) to list product items in a specific category.

package com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository;

//...

/**
 * Demo ProductRepository Implementation.
 */
public class MyDemoProductRepositoryImpl extends AbstractProductRepository {

    @Override
    public ItemModel findOne(CommerceConnector connector, String id, QuerySpec querySpec) throws ConnectorException {
        // Retrieve the internal data first.
        final MyDemoData data = MyDemoDataLoader.getMyDemoData();
        final List<MyDemoProductItem> productItems = data.getResponse().getProductItems();

        // For simplicity, just iterate over the items and return it if an item is found by the product code.
        for (MyDemoProductItem item : productItems) {
            if (id.equals(item.getCode())) {
                return item;
            }
        }

        return null;
    }

    @Override
    public PageResult<ItemModel> findAll(CommerceConnector connector, QuerySpec querySpec) throws ConnectorException {
        // Read the pagination params from querySpec.
        final long offset = querySpec.getOffset();
        final long limit = (querySpec.getLimit() != null) ? querySpec.getLimit().longValue()
                : MyDemoConstants.DEFAULT_PAGE_LIMIT;
        // The item collection to be in the return object.
        final List<ItemModel> itemModels = new LinkedList<>();

        final MyDemoData data = MyDemoDataLoader.getMyDemoData();
        final List<MyDemoProductItem> productItems = data.getResponse().getProductItems();

        final long totalSize = productItems.size();
        final long endOffset = Math.min(offset + limit, totalSize);

        // Put only the items in the index range of the specified page.
        for (int index = (int) offset; index < endOffset; index++) {
            itemModels.add(productItems.get(index));
        }

        // Return a paginated result using the common SimplePageResult class with item collection and pagination info.
        return new SimplePageResult<>(itemModels, offset, limit, totalSize);
    }

    @Override
    public PageResult<ItemModel> findAllByCategory(CommerceConnector connector, CategoryForm categoryForm,
            QuerySpec querySpec) throws ConnectorException {
        final long offset = querySpec.getOffset();
        final long limit = (querySpec.getLimit() != null) ? querySpec.getLimit().longValue()
                : MyDemoConstants.DEFAULT_PAGE_LIMIT;
        final String categoryId = categoryForm.getId();

        final List<ItemModel> itemModels = new LinkedList<>();

        final MyDemoData data = MyDemoDataLoader.getMyDemoData();
        // Almost same as #findAll(...), but let's filter the items only for the specific category.
        final List<MyDemoProductItem> productItems = data.getResponse().getProductItems().stream()
                .filter(item -> item.getCategories().contains(categoryId)).collect(Collectors.toList());

        final long totalSize = productItems.size();
        final long endOffset = Math.min(offset + limit, totalSize);

        for (int index = (int) offset; index < endOffset; index++) {
            itemModels.add(productItems.get(index));
        }

        // Return a paginated result using the common SimplePageResult class with item collection and pagination info. 
        return new SimplePageResult<>(itemModels, offset, limit, totalSize);
    }
}

This implementation is quite basic, but if you replace some logic with a real backend communication, you will be able to implement something real for a specific Commerce Backend Platform.

If your Commerce Backend Platform supports REST APIs, then you can use CRISP API for the interactions with the backend. CRISP API will make it a lot easier to implement and configure.

Test ProductRepository

Unit testing is important to keep the code quality level high. It is also very helpful for your Commerce Connector Module to be validated at a basic level before spending effort later in the integration testing phase.

Suppose you want to make sure that your ProductRepository implementation works properly based on inputs such as pagination information. You will probably need to instantiate your ProductRepository, make some calls on it to test with various inputs, and finally validate the outcomes in each scenario like the following example:

package com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository;

//...

/**
 * Unit tests for my ProductRepository implementation.
 */
public class MyDemoProductRepositoryImplTest extends AbstractMyDemoRepositoryTest {

    private ProductRepository productRepository;

    @Before
    public void setUp() throws Exception {
        productRepository = new MyDemoProductRepositoryImpl();
    }

    @Test
    public void testFindAll() throws Exception {
        // Create a mock CommerceConnector instance which simply sets the default (CRISP) resource space name
        // even if CRISP is not used in our demo module. See AbstractMyDemoRepositoryTest#createMockCommerceConnector()
        // for detail on how it can create a mock CommerceConnector and CommerceConnectorComponent instances using EasyMock.
        final CommerceConnector mockConnector = createMockCommerceConnector("mydemoSpace");

        // Create a QuerySpec with default pagination info,
        // and invoke the ProductRepository with that.
        QuerySpec querySpec = new QuerySpec();
        PageResult<ItemModel> pageResult = productRepository.findAll(mockConnector, querySpec);

        // Check the paginated result starting at the zero index.
        assertEquals(0, pageResult.getOffset());
        assertEquals(MyDemoConstants.DEFAULT_PAGE_LIMIT, pageResult.getLimit());
        assertEquals(10, pageResult.getSize());
        assertEquals(10, pageResult.getTotalSize());

        // Also, check the first product item in the result collection.
        ItemModel itemModel = pageResult.iterator().next();
        assertEquals("WOMENS_M-Class_TEE", itemModel.getId());
        assertEquals("97115", itemModel.getCode());
        assertEquals("Women's M-Class Tee", itemModel.getDisplayName());
        assertTrue(itemModel.getDescription().startsWith("Vestri M-Class logo anchors the signature web stripes racing"));
        assertEquals(new BigDecimal("49.99"), itemModel.getListPrice().getMoneyAmounts().get(0).getAmount());
        assertEquals(new BigDecimal("49.99"), itemModel.getPurchasePrice().getMoneyAmounts().get(0).getAmount());
        assertEquals("https://s3-us-west-2.amazonaws.com/elasticpath-demo-images/VESTRI_VIRTUAL/97115.png",
                itemModel.getImageSet().getThumbnail().getSelfLink().getHref());

        // Create a QuerySpec with a specific pagination info in range [5, 10),
        // and invoke the ProductRepository with that. 
        querySpec = new QuerySpec(5L, 5L);
        pageResult = productRepository.findAll(mockConnector, querySpec);

        // Check the paginated result starting at the 5th index. 
        assertEquals(5, pageResult.getOffset());
        assertEquals(5, pageResult.getLimit());
        assertEquals(5, pageResult.getSize());
        assertEquals(10, pageResult.getTotalSize());

        // Also, check the first product item in the result collection.
        itemModel = pageResult.iterator().next();
        assertEquals("AUTO_DRIVE", itemModel.getId());
        assertEquals("11610", itemModel.getCode());
        assertEquals("AutoPilot", itemModel.getDisplayName());
        assertTrue(itemModel.getDescription().startsWith("All Vestri vehicles produced, have the ability for full self-driving"));
        assertEquals(new BigDecimal("775.0"), itemModel.getListPrice().getMoneyAmounts().get(0).getAmount());
        assertEquals(new BigDecimal("775.0"), itemModel.getPurchasePrice().getMoneyAmounts().get(0).getAmount());
        assertEquals("https://s3-us-west-2.amazonaws.com/elasticpath-demo-images/VESTRI_VIRTUAL/11610.png",
                itemModel.getImageSet().getThumbnail().getSelfLink().getHref());
    }

    // ...

}

Now, you are able to implement, test, and validate your ProductRepository implementation, without having to test everything through a real application deployment from the beginning.

Implement CustomerRepository

To support customer's sign-up, sign-in, and sign-out processes in Bloomreach Commerce Accelerator Applications, a CustomerRepository implementation should be provided like the following example:

package com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository;

//...

/**
 * Demo CustomerRepository Implementation.
 */
public class MyDemoCustomerRepositoryImpl extends AbstractCustomerRepository {

    /**
     * Let's keep the customer map by id in-memory here, initially empty.
     * So you need to sign-up first whenever once restarted.
     * Simple enough for the demo.
     */
    private Map<String, MyDemoCustomerModel> customerModelsById = new ConcurrentHashMap<>();

    /**
     * Let's keep the customer map by e-mail in-memory here, initially empty.
     * So you need to sign-up first whenever once restarted.
     * Simple enough for the demo.
     */
    private Map<String, MyDemoCustomerModel> customerModelsByEmail = new ConcurrentHashMap<>();

    @Override
    public CustomerModel findOne(CommerceConnector connector, String id, QuerySpec querySpec) throws ConnectorException {
        // For demo purpose, let's disallow to find customer profile if id is blank.
        if (StringUtils.isBlank(id)) {
            throw new IllegalArgumentException("Blank customer id.");
        }

        CustomerModel customerModel = customerModelsById.get(id);

        if (customerModel == null) {
            throw new ConnectorException("401", "Customer not authenticated.");
        }

        return customerModel;
    }

    @Override
    public PageResult<CustomerModel> findAll(CommerceConnector connector, QuerySpec querySpec) throws ConnectorException {
        throw new UnsupportedOperationException();
    }

    @Override
    public CustomerModel save(CommerceConnector connector, CustomerForm resourceForm) throws ConnectorException {
        // For demo purpose, let's disallow to save customer profile if e-mail address is blank.
        if (StringUtils.isBlank(resourceForm.getEmail())) {
            throw new IllegalArgumentException("Blank customer E-Mail address.");
        }

        // Retrieve an existing customerModel from the in-memory map.
        final MyDemoCustomerModel customerModel = customerModelsByEmail.get(resourceForm.getEmail());

        // If not existing, no customer exists in our demo.
        if (customerModel == null) {
            throw new ConnectorException("404", "Customer not found.");
        }

        // Let's update the model directly in the in-memory map.
        customerModel.setFirstName(resourceForm.getFirstName());
        customerModel.setLastName(resourceForm.getLastName());

        return customerModel;
    }

    @Override
    public CustomerModel create(CommerceConnector connector, CustomerForm resourceForm) throws ConnectorException {
        // For demo purpose, let's disallow to sign up customer if e-mail address is blank.
        if (StringUtils.isBlank(resourceForm.getEmail())) {
            throw new IllegalArgumentException("Blank customer's E-Mail address.");
        }

        // Let's create a customer model with a random ID and setting the other properties by the input.
        final MyDemoCustomerModel customerModel = new MyDemoCustomerModel(UUID.randomUUID().toString());
        customerModel.setEmail(resourceForm.getEmail());
        customerModel.setFirstName(resourceForm.getFirstName());
        customerModel.setLastName(resourceForm.getLastName());
        // setting a visitor specific access token, just for demonstration purpose, but will not be used in this demo.
        customerModel.setAccessToken(UUID.randomUUID().toString());

        // OK, let's register the new customer model in the in-memory map.
        customerModelsById.put(customerModel.getId(), customerModel);
        customerModelsByEmail.put(resourceForm.getEmail(), customerModel);

        return customerModel;
    }

    @Override
    public CustomerModel delete(CommerceConnector connector, String resourceId) throws ConnectorException {
        // We don't support customer removal in this demo.
        throw new UnsupportedOperationException();
    }

    @Override
    public CustomerModel checkIn(CommerceConnector connector, CustomerForm resourceForm) throws ConnectorException {
        // For demo purpose, let's disallow to sign in customer if e-mail address is blank.
        if (StringUtils.isBlank(resourceForm.getEmail())) {
            throw new IllegalArgumentException("Blank customer's E-Mail address.");
        }

        // CustomerRepository#checkIn(...) is invoked when Bloomreach Commerce Accelerator Application wants the customer to sign in.
        // For simplicity in our demo, let's just the customer signed in without having to check the password
        // if the customer model is found in the in-memory map.
        final MyDemoCustomerModel customerModel = customerModelsByEmail.get(resourceForm.getEmail());

        if (customerModel == null) {
            throw new ConnectorException("401", "Customer not authenticated.");
        }

        return customerModel;
    }

    @Override
    public CustomerModel checkOut(CommerceConnector connector, CustomerForm resourceForm) throws ConnectorException {
        // For demo purpose, let's disallow to sign out customer if e-mail address is blank.
        if (StringUtils.isBlank(resourceForm.getEmail())) {
            throw new IllegalArgumentException("Blank customer's E-Mail address.");
        }

        // More advanced Commerce Connector Module might want to update the customer states in the backend
        // when a customer wants to sign out.
        // But in our demo, let's just return the customer model for simplicity.
        return customerModelsByEmail.get(resourceForm.getEmail());
    }
}

Test CustomerRepository

Here is an example unit test code for the CustomerRepository implementation:

package com.bloomreach.commercedxp.demo.connectors.mydemoconnector.repository;

//...

/**
 * Unit tests for my CustomerRepository implementation.
 */
public class MyDemoCustomerRepositoryImplTest extends AbstractMyDemoRepositoryTest {

    private CustomerRepository customerRepository;

    @Before
    public void setUp() throws Exception {
        customerRepository = new MyDemoCustomerRepositoryImpl();
    }

    @Before
    public void testCheckInWithoutSignup() throws Exception {
        // Create a mock CommerceConnector instance which simply sets the default (CRISP) resource space name
        // even if CRISP is not used in our demo module. See AbstractMyDemoRepositoryTest#createMockCommerceConnector()
        // for detail on how it can create a mock CommerceConnector and CommerceConnectorComponent instances using EasyMock. 
        final CommerceConnector mockConnector = createMockCommerceConnector("mydemoSpace");

        // Create a simple CustomerForm which is passed along from an application to the CustomerRepository,
        // with customer sign-in information.
        SimpleCustomerForm resourceForm = new SimpleCustomerForm("[email protected]", "password", "mystore");

        try {
            // This sign-in attempt should fail because there's no signed-up customer initially.
            customerRepository.checkIn(mockConnector, resourceForm);
            fail("Not supposed to sign-in by non-registered user.");
        } catch (ConnectorException expected) {
        }
    }

    @Before
    public void testCheckInOutAfterSignup() throws Exception {
        // Create a mock CommerceConnector instance which simply sets the default (CRISP) resource space name
        // even if CRISP is not used in our demo module. See AbstractMyDemoRepositoryTest#createMockCommerceConnector()
        // for detail on how it can create a mock CommerceConnector and CommerceConnectorComponent instances using EasyMock.
        final CommerceConnector mockConnector = createMockCommerceConnector("mydemoSpace");

        // Create a simple CustomerForm which is passed along from an application to the CustomerRepository,
        // with customer sign-up information. 
        SimpleCustomerForm resourceForm = new SimpleCustomerForm("John", "", "Doe", "[email protected]", "password", "password", null);
        // When a customer signing up, Bloomreach Commerce Accelerator Application invokes CustomerRepository#create(...) operation.
        CustomerModel customerModel = customerRepository.create(mockConnector, resourceForm);

        // Let's validate the sign-up outcome, which should be a valid CustomerModel object
        // with the same values given by the CustomerForm object.
        assertEquals("John", customerModel.getFirstName());
        assertEquals("Doe", customerModel.getLastName());
        assertEquals("[email protected]", customerModel.getEmail());

        // All right. Let's try to sign-in with the customer.
        resourceForm = new SimpleCustomerForm("[email protected]", "password", "mystore");
        // When a customer signing in, Bloomreach Commerce Accelerator Application invokes CustomerRepository#checkIn(...) operation.
        customerModel = customerRepository.checkIn(mockConnector, resourceForm);

        // Let's validate the sign-in outcome, which should be a valid CustomerModel object.
        assertEquals("John", customerModel.getFirstName());
        assertEquals("Doe", customerModel.getLastName());
        assertEquals("[email protected]", customerModel.getEmail());

        // Now, let's sign-out.
        // When a customer signing out, Bloomreach Commerce Accelerator Application invokes CustomerRepository#checkOut(...) operation.
        customerModel = customerRepository.checkOut(mockConnector, resourceForm);

        // Let's validate the sign-out outcome, which should be the same valid CustomerModel object again.
        assertEquals("John", customerModel.getFirstName());
        assertEquals("Doe", customerModel.getLastName());
        assertEquals("[email protected]", customerModel.getEmail());
    }
}

Now, you are able to implement, test, and validate your CustomerRepository implementation, without having to test everything through a real application deployment from the beginning.

The Rest

You can find other implementations and test cases in the Demo Commerce Connector Module project located at https://github.com/onehippo/demo-commerce-connector. e.g, Example CartRepository implementation and its test case. Fork it and put your hands in.

Integration Testing

For B2C Commerce Connector module, see Integration Testing with New B2C Commerce Connector page.

For B2B Commerce Connector module, see Integration Testing with New B2B Commerce Connector page.

Summary

You can develop a Commerce Connector Module to integrate a Commerce Backend Platform with Bloomreach Commerce Accelerator Application seamlessly, by following the guides and examples in the Demo Commerce Connector Module project located at https://github.com/onehippo/demo-commerce-connector which contains both B2C Demo Commerce Connector Module and B2B Demo Commerce Connector Module implementations. Not only does the demo project explain how to implement the CommerceRepository components, it also gives concrete ideas on how to test your components with unit tests for basic validations, without having to test everything through deployed applications from the beginning. Performing integration tests for your Commerce Connector Module with Bloomreach B2C Commerce Accelerator project or Bloomreach B2B Commerce Accelerator project is also straightforward.

Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?