com.adobe.cq.commerce.pim.common.AbstractProductImporter Maven / Gradle / Ivy
/*************************************************************************
*
* ADOBE CONFIDENTIAL
* __________________
*
* Copyright 2012 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.adobe.cq.commerce.pim.common;
import java.io.IOException;
import java.util.Calendar;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.servlet.ServletException;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.util.Text;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import aQute.bnd.annotation.ConsumerType;
import com.adobe.cq.commerce.api.conf.CommerceBasePathsService;
import com.adobe.cq.commerce.pim.api.ProductImporter;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.i18n.I18n;
import static com.adobe.cq.commerce.api.CommerceConstants.PN_COMMERCE_TYPE;
import static com.day.cq.commons.jcr.JcrConstants.JCR_LASTMODIFIED;
/**
* An abstract base class used for writing product importers.
*/
@Component(componentAbstract = true, metatype = true)
@Service
@ConsumerType
public abstract class AbstractProductImporter extends AbstractImporter implements ProductImporter {
private static final Logger log = LoggerFactory.getLogger(AbstractProductImporter.class);
/**
* The location where imported product stores are created.
*/
protected String basePath;
/**
* The maximum number of children allowed to be imported into a single section before
* buckets are created. (Also the maximum allowed in each bucket.)
*
* Configurable via the "Bucket Size" property of the concrete OSGi component. Defaults to 500.
*/
private int BUCKET_MAX;
private static final int DEFAULT_BUCKET_SIZE = 500;
@Property(label = "Bucket Size", description = "Maximum products per section before bucketing, and maximum in each bucket",
intValue = DEFAULT_BUCKET_SIZE)
public static final String BUCKET_SIZE_PROP_NAME = "cq.commerce.productimporter.bucketsize";
/**
* The node name used when buckets are required.
* Conceptually a constant, but implemented without static or final in case subclasses want
* to override.
*/
protected String NN_BUCKET = "bucket";
/**
* The node type used when buckets are required.
* Conceptually a constant, but implemented without static or final in case subclasses want
* to override.
*/
protected String NT_BUCKET = "sling:Folder";
private int productCount; // number of product nodes created
private int variationCount; // number of product variation nodes created
@Activate
protected void activate(ComponentContext ctx) throws Exception {
super.activate(ctx);
BUCKET_MAX = PropertiesUtil.toInteger(ctx.getProperties().get(BUCKET_SIZE_PROP_NAME), DEFAULT_BUCKET_SIZE);
// We cannot inject the service with @Reference because it doesn't work in classes extending this class
BundleContext btx = ctx.getBundleContext();
ServiceReference ref = btx.getServiceReference(CommerceBasePathsService.class.getName());
CommerceBasePathsService cbps = (CommerceBasePathsService) btx.getService(ref);
basePath = cbps.getProductsBasePath();
btx.ungetService(ref);
}
public void importProducts(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
if (!validateInput(request, response)) {
return;
}
final I18n i18n = new I18n(request);
ResourceResolver resourceResolver = request.getResourceResolver();
Session session = resourceResolver.adaptTo(Session.class);
String storeName = request.getParameter("storeName");
String storePath = request.getParameter("storePath");
String provider = request.getParameter("provider");
initTicker(request.getParameter("tickertoken"), session);
Boolean incrementalImport = false;
if (request.getParameter("incrementalImport") != null) {
incrementalImport = true;
}
productCount = 0;
variationCount = 0;
run(resourceResolver, storePath != null ? storePath : basePath, storeName, incrementalImport, provider);
long millis = System.currentTimeMillis() - startTime;
long seconds = millis / 1000;
if (seconds > 120) {
log.info("Imported " + productCount + " products in " + seconds / 60 + " minutes.");
} else {
log.info("Imported " + productCount + " products in " + seconds + " seconds.");
}
String summary = i18n.get("{0} products and {1} variants created/updated.","0 is product count and 1 is variation count, both numbers",productCount,variationCount);
if (getErrorCount() > 0) {
summary += " " + i18n.get("{0} errors encountered.","0 is number of errors",getErrorCount());
}
respondWithMessages(response, summary);
}
/**
* Validate any input required to run the importer. Implementation to be supplied by concrete classes.
*/
protected abstract boolean validateInput(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException;
private void demoteProductChildrenToBucket(Node parent, Session session) throws RepositoryException {
Node bucket = JcrUtil.createUniqueNode(parent, NN_BUCKET, NT_BUCKET, session);
NodeIterator children = parent.getNodes();
long productCount = 0;
while (children.hasNext()) {
Node child = (Node) children.next();
if (child.hasProperty("cq:commerceType") && child.getProperty("cq:commerceType").getString().equals("product")) {
String oldPath = child.getPath();
String newPath = JcrUtil.copy(child, bucket, child.getName()).getPath();
child.remove();
updateLoggedEvents(oldPath, newPath);
productCount++;
}
}
bucket.setProperty("cq:importCount", productCount);
parent.setProperty("cq:importCount", (Value) null);
}
/**
* Creates a product node at a given absolute path. Intervening nodes will be created as sling:Folder,
* as will any bucket nodes required to honor BUCKET_MAX (unless NT_BUCKET has been overridden).
*
* Updates the message and event queues; saves the session if the save batch size has been reached.
*/
protected Node createProduct(String path, Session session) throws RepositoryException {
//
// Fetch or create parent:
//
String parentPath = Text.getRelativeParent(path, 1);
Node parent = JcrUtil.createPath(parentPath, false, "sling:Folder", "sling:Folder", session, false);
//
// If we're already bucketing, get the current bucket:
//
boolean bucketing = false;
if (parent.hasProperty("cq:importBucket")) {
parent = parent.getNode(parent.getProperty("cq:importBucket").getString());
bucketing = true;
}
//
// Will our addition overflow the parent or current bucket?
//
long count = parent.hasProperty("cq:importCount") ? parent.getProperty("cq:importCount").getLong() + 1 : 1;
if (count > BUCKET_MAX) {
//
// If we haven't yet started bucketing, then we need to demote all the existing products to an
// initial bucket.
//
if (!bucketing) {
demoteProductChildrenToBucket(parent, session);
} else {
parent = parent.getParent();
}
//
// Now create a new bucket and register it in the parent:
//
Node bucket = JcrUtil.createUniqueNode(parent, NN_BUCKET, NT_BUCKET, session);
parent.setProperty("cq:importBucket", bucket.getName());
parent = bucket;
count = 1;
}
parent.setProperty("cq:importCount", count);
//
// Now carry on creating the product:
//
Node product = JcrUtil.createUniqueNode(parent, Text.getName(path), "nt:unstructured", session);
product.setProperty(PN_COMMERCE_TYPE, "product");
product.setProperty("sling:resourceType", "commerce/components/product");
product.setProperty(JCR_LASTMODIFIED, Calendar.getInstance());
productCount++;
logEvent("com/adobe/cq/commerce/pim/PRODUCT_ADDED", product.getPath());
logMessage("Created product " + product.getPath(), false);
updateTicker(makeTickerMessage());
checkpoint(session, false);
return product;
}
protected String makeTickerMessage() {
return productCount + " products imported/updated";
}
/**
* Should be called after updating a product.
*
* Updates the message and event queues; saves the session if the save batch size has been reached.
*/
protected void productUpdated(Node product) throws RepositoryException {
productCount++;
logEvent("com/adobe/cq/commerce/pim/PRODUCT_MODIFIED", product.getPath());
logMessage("Updated product " + product.getPath(), false);
updateTicker(makeTickerMessage());
checkpoint(product.getSession(), false);
}
/**
* Should be called after deleting a product.
* *
* Updates the message and event queues; saves the session if the save batch size has been reached.
*/
protected void productDeleted(Node product) throws RepositoryException {
logEvent("com/adobe/cq/commerce/pim/PRODUCT_DELETED", product.getPath());
logMessage("Deleted product " + product.getPath(), false);
updateTicker(makeTickerMessage());
checkpoint(product.getSession(), false);
}
/**
* Creates a variation with a given name within a given product.
*
* Updates the message and event queues; saves the session if the save batch size has been reached.
*/
protected Node createVariant(Node parentProduct, String name) throws RepositoryException {
Node variant = JcrUtil.createUniqueNode(parentProduct, name, "nt:unstructured", parentProduct.getSession());
variant.setProperty(PN_COMMERCE_TYPE, "variant");
variant.setProperty("sling:resourceType", "commerce/components/product");
variant.setProperty(JCR_LASTMODIFIED, Calendar.getInstance());
variationCount++;
Node baseProduct = getBaseProduct(parentProduct);
if (baseProduct != null) {
logEvent("com/adobe/cq/commerce/pim/PRODUCT_MODIFIED", baseProduct.getPath());
}
logMessage("Created variation " + variant.getPath(), false);
checkpoint(parentProduct.getSession(), false);
return variant;
}
/**
* Should be called after updating a variant.
*
* Updates the message and event queues; saves the session if the save batch size has been reached.
*/
protected void variantUpdated(Node variant) throws RepositoryException {
variationCount++;
Node baseProduct = getBaseProduct(variant);
if (baseProduct != null) {
logEvent("com/adobe/cq/commerce/pim/PRODUCT_MODIFIED", baseProduct.getPath());
}
logMessage("Updated variation " + variant.getPath(), false);
updateTicker(makeTickerMessage());
checkpoint(variant.getSession(), false);
}
/**
* Creates a commerce/components/product/image image node for a given product or variation node. It is the
* caller's responsibility to set any properties on the returned image (such as the fileReference).
*
* Updates the message and event queues; saves the session if the save batch size has been reached.
*/
protected Node createImage(Node product) throws RepositoryException {
Node image = product.addNode("image", "nt:unstructured");
image.setProperty("sling:resourceType", "commerce/components/product/image");
image.setProperty(JCR_LASTMODIFIED, Calendar.getInstance());
Node baseProduct = getBaseProduct(product);
if (baseProduct != null) {
logEvent("com/adobe/cq/commerce/pim/PRODUCT_MODIFIED", baseProduct.getPath());
}
logMessage("Created image " + image.getPath(), false);
checkpoint(product.getSession(), false);
return image;
}
/**
* Returns the base product node of a product or variation.
*/
protected Node getBaseProduct(Node node) throws RepositoryException {
while (node != null && !node.getProperty(PN_COMMERCE_TYPE).getString().equals("product")) {
node = node.getParent();
}
return node;
}
}