Friday 11 November 2016

Mocking Static Classes with Powermock

I always forget how to do this and have to look it up every time so here is a quick example for future reference!

- Annotate the class with @RunWith(PowerMockRunner.class)
- Annotate the class with @PrepareForTest(<class-to-mock>.class)
- Mock the static in @Before or in @Test PowerMockito.mockStatic(<class-to-mock>.class)
- Set expectations with when(<class-to-mock.class>.something()).thenReturn(something_else)

Example

This is a simple example which mocks StringUtils (apache commons-lang3).  This shows that the StringUtils.isBlank() method can be mocked to return different values.

    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertTrue;
    import static org.mockito.Mockito.when;

    import org.apache.commons.lang3.StringUtils;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.powermock.api.mockito.PowerMockito;
    import org.powermock.core.classloader.annotations.PrepareForTest;
    import org.powermock.modules.junit4.PowerMockRunner;

    /**
     * An example of using powermock to mock a static class.
     */
    @RunWith(PowerMockRunner.class)
    @PrepareForTest(StringUtils.class)
    public class ExampleTest {

        /**
         * Test blank when mocked to return that an empty string isn't blank!
         */
        @Test
        public void testBlankMocked()
        {
            // Arrange
            PowerMockito.mockStatic(StringUtils.class);
            when(StringUtils.isBlank("")).thenReturn(false);
        
            // Act
            final boolean blank = StringUtils.isBlank("");

            // Assert
            assertFalse(blank);
        }
    
        /**
         * Test blank using the standard non-mocked behaviour.
         */
        @Test
        public void testBlankNormal()
        {
            // Act
            final boolean blank = StringUtils.isBlank("");

            // Assert
            assertTrue(blank);
        }
    }


Maven Dependencies

The powermock dependencies used for this test are,

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>1.6.5</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito</artifactId>
        <version>1.6.5</version>
        <scope>test</scope>
    </dependency>




Friday 29 July 2016

Angular JS Directive For Decimal Places

Here is a simple example for creating an Angular JS Directive.  In this particular case the input is limited to two decimal places but other code could be created here to limit the input in other ways.

Angular Directive

The directive is in javascript and needs to be imported into the page.

(function() {
    'use strict';

    angular.module('myApp').directive('limitedDecimalPlaces',['$filter', function ($filter) {
    return {
        require: 'ngModel',
        link: function (scope, element, attr, ngModelCtrl) {

            if (!ngModelCtrl) return;

            function round(value)
            {
                return Math.floor(value * 100) / 100;
            }

            ngModelCtrl.$parsers.push(function (value) {
            var cleanValue = value;
                if (value != value.toFixed(2))
                {
                    cleanValue = (value) ? round(value) : value;
                    ngModelCtrl.$setViewValue(cleanValue.toString());
                    ngModelCtrl.$render();
                }
                return cleanValue;
            });
        }
    };
}]);
})();

The ngModelCtrl is the model controller for this angular input.
$parsers is an array of the parsers that are applied to this input.  $parsers.push adds the function to the array (push is a standard Arrays.push() javascript function)

ngModelCtrl.$setViewValue(...) sets the value into the view part and must be a string.  Overall the function rounds the number to two decimal places if necessary and assigns it to the cleanValue.  This is then set into the view and returned so that the model is updated.  Whatever is returned from this function is what is set into the model.

Usage

The standard input can have this directive added to it so that the validation is wired into the input.

    <input type="number" name="myNumber" ng-model="myNumber" limited-decimal-places>

Tuesday 26 July 2016

AEM Sightly Templates

When repetitive code is used within an html page, such as capturing an address, using a Sightly Template is really useful to avoid unnecessary duplication and improve maintenance.

Template

The template can be created at the top of the page as

    <template data-sly-template.address> 
        <div class="address">
            Street:  <input type="text" name="street">
            <br>
            Town: <input type="text" name="town">
            <br>
            Postcode: <input type="text" name="postcode">
        </div>
    </template>

Usage

The template usage is very straight forward.  Simply use the Sightly 'call' function,

    <div data-sly-call="${address}"></div>

Separate File

As templates get bigger or for maintainability a separate file can be used.  AEM will do the work of gathering the template files together for a given page so there is no problem about the paths in AEM being available.

To use a separate file the template must be imported in the html page and then called with the extra selector which defines the file it lives in

address.html
    <template data-sly-template.address> 
        <div class="address">
            Street:  <input type="text" name="street">
            <br>
            Town: <input type="text" name="town">
            <br>
            Postcode: <input type="text" name="postcode">
        </div>
    </template>

Import
Import the template file

    <div data-sly-use.details="address.html"></div>

Use
The usage changes very slightly as it has to reference the import ('details') as well as the template ('address') within the file.

    <div data-sly-call="${details.address}"></div>

Using Parameters

Parameters can also be used within a template.  This is useful if you want the template to behave a particular way or if you want to name the fields slightly differently for each instance of the template.


    <template data-sly-template.address="${@ instance}"> 
        <div class="address">
            Street:  <input type="text" name="${instance}Street">
            <br>
            Town: <input type="text" name="${instance}Town">
            <br>
            Postcode: <input type="text" name="${instance}Postcode">
        </div>
    </template>

Here the instance of the template is passed in so that each of the fields can have a unique name even if the template is used multiple times.  In the example below the Invoice address and Delivery address can have different instances and therefore different names for the fields.


    <div data-sly-call="${details.address @ instance='InvoiceAddress'}"></div>

    <div data-sly-call="${details.address @ instance='DeliveryAddress'}"></div>
This usage of sightly to generate templates can be very useful.

Monday 4 July 2016

AEM QueryBuilder

Here are some examples for using the AEM QueryBuilder.  This took me a bit of time to get used to so it is worth the memory jogger!

Basic Structure

The basic structure uses the AEM adaptTo() to get a QueryBuilder object.

Here the session and builder classes are obtained.  The Query is created with the map of search values (see later).  The SearchResults object would by default be paginated so setting the query.setHitsPerPage(Integer.MAX_VALUE) means we get everything we want.  An iterator is obtained so that we can loop through the results and convert to JSON or further filter if necessary.

    final Session session = request.getResourceResolver().adaptTo(Session.class);
    final QueryBuilder builder = request.getResourceResolver().adaptTo(QueryBuilder.class);
    final Query query = builder.createQuery(PredicateGroup.create(map), session);
    query.setHitsPerPage(Integer.MAX_VALUE);
    final SearchResult result = query.getResult();

    // Iterate over the results
    final Iterator<Resource> resources = result.getResources();



Query Map

The map of values is used to create a PredicateGroup.  This is a group of Predicate objects which are used to match the JCR values against.  The names of the properties will define the predicates that are created for that value.

Create a map to hold the search values.

    final Map<String, Object> map = new HashMap<>();

The 'type' uses the TypePredicate

    map.put("type", "nt:unstructured");

The 'path' uses the PathPredicate

    map.put("path", "/content/mysite/mypages");

The 'boolproperty' defines a JcrBoolPropertyPredicate check. If the search is for 'false' then this is correct if the value is 'false' or not present at all.

    map.put("boolproperty", "live");

    map.put("boolproperty.value", "true");

A numeric range check is possible using the 'rangeproperty'.  This uses the RangePropertyPredicate. There are options for the lower and upper bounds.

    map.put("rangeproperty.property", "age");
    map.put("rangeproperty.lowerBound", age);
    map.put("rangeproperty.lowerOperation", ">=");
Note: even though this is numeric the value provided here should be a string

A number of properties can be matched against.  You can prefix with a #_ to allow multiple properties to be matched on.  These values use a JcrPropertyPredicate

    map.put("1_property", "firstname");
    map.put("1_property.value", "fred");
    map.put("2_property", "surname");
    map.put("2_property.value", "bloggs");

Debugging

Get the Iterator from the PredicateGroup and loop through to see which Predicates have been created for your map.

Always add the values into the map as strings otherwise the predicates don't match.

Use the query debugger to check your map,

    http://localhost:4502/libs/cq/search/content/querydebug.html

Wednesday 15 June 2016

AEM Unit Testing

AEM Unit testing of java code can be a bit of a pain.  Here is an example search servlet and the unit test that goes with it.

Servlet

This is a really simple servlet which searches for resources on a particular path

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;

import javax.management.Query;
import javax.naming.directory.SearchResult;

import org.w3c.dom.Node;

/**
 * Servlet that can be used to search for nodes flagged as current = true on nt:unstructured nodes.
 */
@SlingServlet(paths = "/bin/search/current", methods = "GET")
public class SearchCurrentServlet extends SlingSafeMethodsServlet
{
    /**
     * Default serialisation.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(SearchCurrentServlet.class);

    /**
     * The path to search.
     */
    private static final String SEARCH_PATH = "/content/my/website/data";

    /**
     * The JSON indentation level.
     */
    private static final int JSON_INDENTATION_LEVEL = 4;

    /**
     * The method called by the get.
     * {@inheritDoc}
     */
    @Override
    protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response) throws ServletException, IOException
    {
        // Get the properties that have been requested
        final Map<String, Object> map = new HashMap<>();
        map.put("type", "nt:unstructured");
        map.put("path", SEARCH_PATH);

        map.put("1_property", "current");
        map.put("1_property.value", true);

        // Get the QueryBuilder and do the query from the Map above.
        final Session session = request.getResourceResolver().adaptTo(Session.class);
        final QueryBuilder builder = request.getResourceResolver().adaptTo(QueryBuilder.class);
        final Query query = builder.createQuery(PredicateGroup.create(map), session);
        query.setHitsPerPage(Integer.MAX_VALUE);
        final SearchResult result = query.getResult();

        // Iterate over the results
        final Iterator<Resource> resources = result.getResources();
        try
        {
            while (resources.hasNext())
            {
                // Get the next resource, convert to Node and make it as JSON.
                final Resource resource = resources.next();
                final Node node = resource.adaptTo(Node.class);
                final JsonJcrNode jsonJcrNode = new JsonJcrNode(node, getNodeIgnoreSet());
                response.getOutputStream().println(jsonJcrNode.toString(JSON_INDENTATION_LEVEL));
            }
        }
        catch (final RepositoryException | JSONException e)
        {
            response.getOutputStream().println("Error iterating over the results " + e.getMessage());
            LOG.error("Error iterating over the search results", e);
        }

        response.flushBuffer();
    }
}


Unit Test

The unit test uses AemContext to set up a load of mocks which are needed for the test.  However, the AemContext does not give a QueryBuilder object out of the box so an adapter has to be defined for this.  It is added to the AemContext.

import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.jcr.Session;
import javax.servlet.ServletException;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.testing.mock.sling.ResourceResolverType;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.SearchResult;

import io.wcm.testing.mock.aem.junit.AemContext;

/**
 * Test class for the {@link SearchCurrentServlet}.
 */
@RunWith(MockitoJUnitRunner.class)
public class SearchCurrentServletTest
{
    /**
     * The mocked query builder.
     */
    @Mock
    private QueryBuilder queryBuilder;

    /**
     * The AEM context to test with.
     */
    @Rule
    public final AemContext context = new AemContext(aemContext ->
    {
        aemContext.load().json("/result.json", "/result.json");
        aemContext.registerAdapter(ResourceResolver.class, QueryBuilder.class, queryBuilder);
    }, ResourceResolverType.JCR_MOCK);

    /**
     * Test the output in json includes fields from the node.
     *
     * @throws IOException thrown by the servlet method
     * @throws ServletException thrown by the servlet method
     */
    @Test
    public void testDoGet() throws ServletException, IOException
    {
        // Arrange
        final SearchCurrentServlet servlet = new SearchCurrentServlet();
        final MockSlingHttpServletRequest request = context.request();
        final MockSlingHttpServletResponse response = context.response();

        final Query query = mock(Query.class);
        final SearchResult searchResult = mock(SearchResult.class);
        final Resource resource = context.currentResource("/result.json");
        final List<Resource> results = new ArrayList<>();
        results.add(resource);

        when(queryBuilder.createQuery(any(PredicateGroup.class), any(Session.class))).thenReturn(query);
        when(query.getResult()).thenReturn(searchResult);
        when(searchResult.getResources()).thenReturn(results.iterator());

        // Act
        servlet.doGet(request, response);

        // Assert
        // System.out.println(response.getOutputAsString());
        assertTrue(response.getOutputAsString().contains("\"name\": \"Bob\""));
        assertTrue(response.getOutputAsString().contains("\"cost\": \"12.34\""));
    }
}


Unit Test Data

The AemContext loads a json file from src/test/resources (the classpath) and makes it available for the path specified.  When the resource is loaded with context.currentResource("/result.json"); the Resource object returned is made up of the values in the result.json file.

result.json has only the following lines

{
"jcr:primaryType":"nt:unstructured",
        "current":true,
        "name":"Bob",
        "cost":12.34
}



AEM Multifield for Touch UI Dialogs

Multifields are used to group at field set together within AEM.  In a dialog they can be used to store a variable sized array or create a node hierarchy.

Single Field

For a single field the set up is very simple.  The field that can be added and removed is just put straight under the multifield value,


  <description-lines
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/foundation/form/multifield"
    fieldLabel="Description Lines"
    renderReadOnly="{Boolean}true">
    <field
      jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/form/textfield"
      name="./descriptionLines"/>
  </term-description>

This multifield will store an array of the values entered into the ./descriptionLines value in the JCR.

Field Set

For a genuine set of fields that need to be grouped together the hierarchy under the multifield definition is more complicated,

  <person
    jcr:primaryType="nt:unstructured"
    sling:resourceType="/apps/touch-ui-multi-field-panel/multifield"
    class="full-width"
    fieldDescription="Click '+' to add a new person"
    fieldLabel="People">
    <field
      jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/form/fieldset"
      name="./items">
      <layout
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
        method="absolute"/>
      <items jcr:primaryType="nt:unstructured">
        <column
          jcr:primaryType="nt:unstructured"
          sling:resourceType="granite/ui/components/foundation/container">
          <items jcr:primaryType="nt:unstructured">
            <name
              jcr:primaryType="nt:unstructured"
              sling:resourceType="granite/ui/components/foundation/form/textfield"
              fieldDescription="Enter Name"
              fieldLabel="Name"
              name="./name"/>
            <age
              jcr:primaryType="nt:unstructured"
              sling:resourceType="granite/ui/components/foundation/form/textfield"
              fieldDescription="Enter Age"
              fieldLabel="Age"
              name="./age"/>
          </items>
        </column>
      </items>
    </field>
  </person>


This multifield creates a subnode under ./items each of which contains a 'name' and 'age' field as defined as textfields in the set up.

Thursday 26 May 2016

Creating a SlingModel object

Sling Model objects can be used within Sightly to hold data from the JCR and also to request data from other OSGi resources.  A simple example of a SlingModel object is below.  In this example the Commerce API is used to get product data within the SlingModel so that the data can be displayed within a product page.

You have to be a bit careful with SlingModels as the @Inject will only work for the node that you are on.  For example, if you have a parsys and a component within it then the @SlingObject and the @Inject will be applicable to the component within the parsys rather than the actual page that holds the parsys.  There are two examples of sling models below.  One uses the @Inject to get a property and the other adapts the resource to a PageManager to get the containing page.  This may be useful if you have used the commerce API to create product pages as the cq:productMaster value is on the actual page but the @SlingObject may be a component within a parsys.

Some basic information about the Sling Model can be found here https://sling.apache.org/documentation/bundles/models.html.

Pom.xml

Without checking the pom.xml the sling model may never be wired.  The apache.felix plugin must be told where the class annotated with @Model are so that it creates the correct xml as part of the build of this bundle.

    <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-bundle-plugin</artifactId>
        <extensions>true</extensions>
        <configuration>
            <instructions>
                <Sling-Model-Packages>com.me.sling.model</Sling-Model-Packages>
            </instructions>
        </configuration>
    </plugin>

If this isn't included then the model will never be wired with the OSGiService or any of the @Inject annotations that are available.  Also the @PostConstruct won't be called.  This will result in a NullPointerException when a page tries to use this ProductData class.

SlingModel Class

package com.me.sling.model;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.commerce.api.CommerceException;
import com.adobe.cq.commerce.api.CommerceService;
import com.adobe.cq.commerce.api.CommerceServiceFactory;
import com.adobe.cq.commerce.api.Product;

/**
 * Use the CommerceService to load the product information using an example SlingModel
 */
@Model(adaptables = Resource.class)
public class ProductData
{
    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(ProductData.class);

    /**
     * The commerce service used to load the product.
     */
    @OSGiService(filter = "(service.pid=com.me.MyCommerceServiceFactory)")
    private CommerceServiceFactory commerceServiceFactory;

    /**
     * The product master value.  This is a default value which is created by a RollOut 
     * of a catalog in the commerce api process. This references the product location
     * in the /etc/commerce/products area.
     * 
     */
    @Inject
    @Named("cq:productMaster")
    private String productMaster;

    /**
     * The current resource.
     */
    @SlingObject
    private Resource currentResource;

    /**
     * This is the product.
     */
    private Product product;

    /**
     * Load the product value.
     *
     * @throws CommerceException Thrown if there is a problem loading the product
     */
    @PostConstruct
    public void create() throws CommerceException
    {
        LOG.warn("PostConstruct called");
        LOG.debug("productMaster {}", productMaster);

        LOG.debug("CommerceServiceFactory {}", commerceServiceFactory);

        final CommerceService commerceService = commerceServiceFactory.getCommerceService(currentResource);
        LOG.debug("CommerceService {}", commerceService);

        product = commerceService.getProduct(productMaster);
    }

    /**
     * Get the SKU.
     *
     * @return The SKU / Identifier / Product Code
     */
    public String getSku()
    {
        return product.getSKU();
    }
}

Here the @OSGiService annotation is used to reference a Service.  We use the filter to make sure we get the correct service that we need in case there are duplicate implementations of the interface available.
The @SlingObject is an annotation which just allows the current sling resource to be mapped into this Sling Model.

SlingModel Class (With Parsys)

In this example the component which uses this SlingModel object is within a parsys in the page.  Therefore the @Inject @Named("cq:productMaster") doesn't work because that value lives on the Page rather than on the component.  


package com.me.sling.model;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.commerce.api.CommerceException;
import com.adobe.cq.commerce.api.CommerceService;
import com.adobe.cq.commerce.api.CommerceServiceFactory;
import com.adobe.cq.commerce.api.Product;

/**
 * Use the CommerceService to load the product information using an example SlingModel
 */
@Model(adaptables = Resource.class)
public class ProductData
{
    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(ProductData.class);

    /**
     * The commerce service used to load the product.
     */
    @OSGiService(filter = "(service.pid=com.me.MyCommerceServiceFactory)")
    private CommerceServiceFactory commerceServiceFactory;

    /**
     * The current resource.
     */
    @SlingObject
    private Resource currentResource;

    /**
     * This is the product.
     */
    private Product product;

    /**
     * Load the product value.
     *
     * @throws CommerceException Thrown if there is a problem loading the product
     */
    @PostConstruct
    public void create() throws CommerceException
    {
        LOG.warn("PostConstruct called");
        LOG.debug("productMaster {}", productMaster);

        LOG.debug("CommerceServiceFactory {}", commerceServiceFactory);


        final PageManager pageManager = currentResource.getResourceResolver().adaptTo(PageManager.class);
        final Page currentPage = pageManager.getContainingPage(currentResource);
        LOG.debug("currentPage {}", currentPage.getPath());

        final String productMaster = currentPage.getProperties().get("cq:productMaster", String.class);

        final CommerceService commerceService = commerceServiceFactory.getCommerceService(currentResource);
        LOG.debug("CommerceService {}", commerceService);

        product = commerceService.getProduct(productMaster);
    }

    /**
     * Get the SKU.
     *
     * @return The SKU / Identifier / Product Code
     */
    public String getSku()
    {
        return product.getSKU();
    }
}

Sightly

Use of the Sling Model within sightly is really straightforward.  Just tell Sightly to use this class.  The data-sly-use.model is the name that the data can then be referenced by.  Highlighted below,

    <sly data-sly-use.model="com.me.sling.model.ProductData">

    Page Product Sly
    <p>Title: <span>${properties.jcr:title}</span></p>
    <p>Description: <span>${properties.jcr:description}</span></p>
    <p>Identifier: <span>${model.sku}</span></p>


Thursday 19 May 2016

Searching within CRXDE (AEM)

In CRXDE there is a good search tool.  Go to Tools - Query

    Type: SQL2
    Path: / <or a sub path to limit the search if you want to>
    Text: <The text to search on>

Click Generate to a search that looks something like this,

    SELECT * FROM [nt:base] AS s WHERE CONTAINS(s.*, 'asdf')

This is a default search, click Execute to run it.

If you want to search for a particular value you can manually alter the search and click Execute.  A search for all the cq:commerceProvider values that equal 'geometrixx' is done as follows,

    SELECT * FROM [nt:base] AS s WHERE CONTAINS(s.[cq:commerceProvider], 'geometrixx')


Wednesday 18 May 2016

AEM Commerce Provider

If products are created within the /etc/commerce/products area they should be flagged with a cq:commerceProvider on the subfolder which holds them.

    /etc/commerce/products/andyProds
                                cq:commerceProvider = andyCommerceProvider
                                jcr:primaryType = sling:Folder

However, to see these products within the TouchUI commerce / products pages and to use the scaffolding to edit the values a commerceProvider is needed.

To create a simple commerceProvider is very straightforward and is documented below.

CommerceServiceFactory

This factory class just returns a CommerceService

package com.me.commerce;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.Resource;

import com.adobe.cq.commerce.api.CommerceService;
import com.adobe.cq.commerce.api.CommerceServiceFactory;
import com.adobe.cq.commerce.common.AbstractJcrCommerceServiceFactory;

/**
 * Specific implementation for the {@link CommerceServiceFactory} interface.
 */
@Component
@Service
@Properties(value = {
                @Property(name = "service.description", value = "Factory for implementation of the commerce service"),
                @Property(name = "commerceProvider", value = "meCommerceProvider", propertyPrivate = true)})
public class MeCommerceServiceFactory extends AbstractJcrCommerceServiceFactory implements CommerceServiceFactory
{

    /**
     * Create a new {@link MeCommerceService}.
     *
     * @param res The resource
     * @return The CommerceService found
     */
    @Override
    public CommerceService getCommerceService(final Resource res)
    {
        return new MeCommerceService(getServiceContext(), res);
    }
}

CommerceService

The CommerceService itself doesn't need to do much in our case apart from return a product.

package com.me.commerce;

import java.util.ArrayList;
import java.util.List;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;

import com.adobe.cq.commerce.api.CommerceConstants;
import com.adobe.cq.commerce.api.CommerceException;
import com.adobe.cq.commerce.api.CommerceService;
import com.adobe.cq.commerce.api.CommerceSession;
import com.adobe.cq.commerce.api.Product;
import com.adobe.cq.commerce.common.AbstractJcrCommerceService;
import com.adobe.cq.commerce.common.ServiceContext;

/**
 * This is the commerce service which is used by the Commerce API within AEM. The commerce-provider is a property that
 * is put on the base folder where the products are held.
 */
public class MeCommerceService extends AbstractJcrCommerceService implements CommerceService
{

    /**
     * Construct this MeCommerceService object.
     *
     * @param serviceContext The service context
     * @param resource The resource
     */
    public MeCommerceService(final ServiceContext serviceContext, final Resource resource)
    {
        super(serviceContext, resource);
    }

    /*
     * (non-Javadoc)
     * @see com.adobe.cq.commerce.api.CommerceService#getProduct(java.lang.String)
     */
    @Override
    public Product getProduct(final String path) throws CommerceException
    {
        final Resource resource = resolver.getResource(path);
        if (resource != null && MeCommerceProduct.isAProductOrVariant(resource))
        {
            return new MeCommerceProduct(resource);
        }
        return null;
    }

    /*
     * (non-Javadoc)
     * @see com.adobe.cq.commerce.api.CommerceService#isAvailable(java.lang.String)
     */
    @Override
    public boolean isAvailable(final String serviceType)
    {
        return CommerceConstants.SERVICE_COMMERCE.equals(serviceType);
    }

    /*
     * (non-Javadoc)
     * @see com.adobe.cq.commerce.api.CommerceService#login(org.apache.sling.api.SlingHttpServletRequest,
     * org.apache.sling.api.SlingHttpServletResponse)
     */
    @Override
    public CommerceSession login(final SlingHttpServletRequest request, final SlingHttpServletResponse response) throws CommerceException
    {
        return null;
    }

    /*
     * (non-Javadoc)
     * @see com.adobe.cq.commerce.api.CommerceService#getCountries()
     */
    @Override
    public List<String> getCountries() throws CommerceException
    {
        final List<String> countries = new ArrayList<String>();
        countries.add("*");
        return countries;
    }

    /*
     * (non-Javadoc)
     * @see com.adobe.cq.commerce.api.CommerceService#getCreditCardTypes()
     */
    @Override
    public List<String> getCreditCardTypes() throws CommerceException
    {
        return new ArrayList<String>();
    }

    /*
     * (non-Javadoc)
     * @see com.adobe.cq.commerce.api.CommerceService#getOrderPredicates()
     */
    @Override
    public List<String> getOrderPredicates() throws CommerceException
    {
        return new ArrayList<String>();
    }
}

CommerceProduct

The commerce product doesn't need to do anything more than return the SKU which is a required field in the AEM commerce section.

package com.me.commerce;

import org.apache.sling.api.resource.Resource;

import com.adobe.cq.commerce.api.Product;
import com.adobe.cq.commerce.common.AbstractJcrProduct;

/**
 * The CommerceProduct needed for the products. This is part of the Commerce framework in AEM.
 */
public class MeCommerceProduct extends AbstractJcrProduct implements Product
{
    /**
     * The name of the identifier.
     */
    public static final String PN_IDENTIFIER = "identifier";

    /**
     * Construct this MeCommerceProduct object.
     *
     * @param resource The resource
     */
    public MeCommerceProduct(final Resource resource)
    {
        super(resource);
    }

    /*
     * (non-Javadoc)
     * @see com.adobe.cq.commerce.api.Product#getSKU()
     */
    @Override
    public String getSKU()
    {
        return getProperty(PN_IDENTIFIER, String.class);
    }
}