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





Tuesday 17 May 2016

AEM eCommerce / Products

It is possible to import product data into AEM using the example geometrixx solution.  The example given imports a csv file using the ProductImporter interface.  However, it is actually much simpler to just create the product nodes manually.

Import Code

The following code can be used to create a product in the correct area in the JCR.  It could be called multiple times as a loop through a CSV, XML or WebService feed

    // Create the product node
    final String testPath = "/etc/commerce/products/test";
    final Resource testPathResource = resourceResolver.getResource(testPath);

    // Get the test path as a Node object
    final Node testJcrNode = testPathResource.adaptTo(Node.class);

    // Add a child node on the test node
    final Node productNode = testJcrNode.addNode("prodABC", "nt:unstructured");

    // Add the properties to the product node created
    productNode.setProperty("cq:commerceType", "product");
    productNode.setProperty("sling:resourceType", "commerce/components/product");
    productNode.setProperty("cq:tags", new String[] {"my:mortgage"});
    productNode.addMixin("cq:Taggable");

    productNode.setProperty("jcr:title", "Product ABC");
    productNode.setProperty("identifier", "ABC");

    ... other properties can be added

Note: If you don't include the addMixin("cq:Taggable") then the rollout of a catalog that includes match criteria based on the tags will not work even if a tab is present.

Subfolders

Subfolders can be created as part of the process above is that is needed.  The easiest way to create a subfolder is to use the ResourceResolver.

In the code below the parentResource has a new subfolder created with the name 'title'.

    final Resource parentResource = resourceResolver.getResource(path);
    resourceResolver.create(parentResource, title, new HashMap<>());

This will create a folder of

    primaryType = sling:Folder 

Monday 9 May 2016

AEM Creating and Updating JCR Nodes

There are a number of ways to create nodes within AEM.

ResourceResolver

Use the resource resolver to get a parent resource then create a child in it

    final Resource parentResource = resourceResolver.getResource(parentPath);
    resourceResolver.create(parentResource, title, new HashMap<>());

This creates a node of sling:folder type.

This is fairly easy to unit test because the resourceResolver could be mocked either directly or using the SlingContext (https://sling.apache.org/documentation/development/sling-mock.html)

Node

You can simply add one node from another node.

    final Resource parentPathResource = resourceResolver.getResource(parentPath);
    final Node parentJcrNode = parentPathResource .adaptTo(Node.class);
    final Node newNode = parentJcrNode.addNode(title, "nt:unstructured");

This is probably the most straight forward way to create a node of a particular primary type.  Here the type created is a nt:unstructured.

PageManager

The PageManager can be used to create actual pages in the jcr similar to below

    final Page page = pageManager.create(parentPath, childName, "", childName);

This creates a type of cq:Page.  This is also easy to mock and test.  Using this form also creates a jcr:content child node off the page which is useful in most instances.

JcrUtil

The JcrUtil gives some great ways to manipulate the nodes.  You can createPath() like here or createUniqueNode(),

    Node parent = JcrUtil.createPath(parentPath, false, "sling:Folder", "sling:Folder", session, false);

However, the session object is needed and this is harder to unit test because the implementation is hidden.

jcr:content

Creating and setting properties on the jcr:content node is a simple case of getting the child from the resource object.

    final Resource childResource = parentResource.getChild(JcrConstants.JCR_CONTENT);
    final Node childNode = childResource .adaptTo(Node.class);
    childNode.setProperty("jcr:description", "Description of the node");