All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.cq.commerce.pim.common.AbstractImporter 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.io.PrintWriter;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import aQute.bnd.annotation.ConsumerType;
import com.adobe.cq.commerce.api.CommerceConstants;
import com.day.cq.commons.jcr.JcrConstants;
import org.apache.commons.lang.StringUtils;
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.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.launcher.ConfigEntry;
import com.adobe.granite.workflow.launcher.WorkflowLauncher;
import com.day.cq.commons.jcr.JcrObservationThrottle;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.tagging.InvalidTagFormatException;
import com.day.cq.tagging.TagManager;

/**
 * An abstract base class used for writing commerce importers.
 */
@Component(componentAbstract = true, metatype = true)
@Service
@ConsumerType
public abstract class AbstractImporter  {
    private static final Logger log = LoggerFactory.getLogger(AbstractImporter.class);

    @Reference
    EventAdmin eventAdmin = null;

    /**
     * The (approximate) number of nodes to create in a single batch before saving the session.
     *
     * Configurable via the "Save Batch Size" property of the concrete OSGi component.  Defaults to 1,000.
     */
    private int SAVE_BATCH_SIZE;

    private static final int DEFAULT_SAVE_BATCH_SIZE = 1000;

    @Property(label = "Save Batch Size", description = "Approximate number of nodes to batch between session saves",
            intValue = DEFAULT_SAVE_BATCH_SIZE)
    public static final String SAVE_BATCH_SIZE_PROP_NAME = "cq.commerce.importer.savebatchsize";

    /**
     * The (approximate) number of nodes to create before pausing to allow the observation manager to catch up.
     *
     * Configurable via the "Throttle Batch Size" property of the concrete OSGi component.  Defaults to 50,000.
     */
    private int THROTTLE_BATCH_SIZE;

    private static final int DEFAULT_THROTTLE_BATCH_SIZE = 50000;

    @Property(label = "Throttle Batch Size", description = "Approximate number of nodes between pauses for observation manager",
            intValue = DEFAULT_THROTTLE_BATCH_SIZE)
    public static final String THROTTLE_BATCH_SIZE_PROP_NAME = "cq.commerce.importer.throttlebatchsize";

    /**
     * The (approximate) maximum number of paths included in each PRODUCT_PAGE_ADDED, PRODUCT_PAGE_MODIFIED or
     * PRODUCT_PAGE_DELETED event.
     */
    private int EVENT_BATCH_SIZE = 1000;

    /**
     * The number of messages allowed in the response.
     * Large imports generate too many messages for the browser to render in reasonable time, but individual
     * messages can be useful when testing on smaller imports.
     *
     * Configurable via the "Message Cap" property of the concrete OSGi component.  Defaults to 1,000.  Set
     * to 0 to return only summary messages.
     */
    private int MESSAGE_CAP;

    private static final int DEFAULT_MESSAGE_CAP = 1000;

    @Property(label = "Message Cap", description = "Maximum number of messages to return in response",
            intValue = DEFAULT_MESSAGE_CAP)
    public static final String MESSAGE_CAP_PROP_NAME = "cq.commerce.importer.messagecap";


    private int saveBatchCount = 0;
    private int throttleBatchCount = 0;
    private JcrObservationThrottle throttle = null;
    private Set disabledWorkflows = new HashSet();

    private Map> eventQueues = new HashMap>();

    private List messages;
    private int errorCount;

    private String tickerToken = null;
    private String tickerMessage;
    private boolean tickerComplete;

    @Activate
    protected void activate(ComponentContext ctx) throws Exception {
        SAVE_BATCH_SIZE = PropertiesUtil.toInteger(ctx.getProperties().get(SAVE_BATCH_SIZE_PROP_NAME), DEFAULT_SAVE_BATCH_SIZE);
        THROTTLE_BATCH_SIZE = PropertiesUtil.toInteger(ctx.getProperties().get(THROTTLE_BATCH_SIZE_PROP_NAME), DEFAULT_THROTTLE_BATCH_SIZE);
        MESSAGE_CAP = PropertiesUtil.toInteger(ctx.getProperties().get(MESSAGE_CAP_PROP_NAME), DEFAULT_MESSAGE_CAP);
    }

    /**
     * Execute the main body of the importer.  This method sets up the store itself, and brackets the concrete
     * implementation's doImport method with performance-enhancing facilities.
     * @param resourceResolver
     * @param basePath
     * @param storeName
     * @param incrementalImport Indicates import should be applied to existing store as a delta
     * @param provider the commerceProvider to be used for the store
     */
    protected void run(ResourceResolver resourceResolver, String basePath, String storeName, boolean incrementalImport, String provider) {

        messages = new ArrayList();
        errorCount = 0;

        try {
            Node rootNode = setupStore(resourceResolver, basePath, storeName, !incrementalImport, provider);

            disableWorkflows(resourceResolver);
            openThrottle(rootNode);

            doImport(resourceResolver, rootNode, incrementalImport);
        } catch (Exception e) {
            log.error("Error while running import", e);
        } finally {
            tickerComplete = true;
            checkpoint(resourceResolver.adaptTo(Session.class), true);
            closeThrottle();
            reenableWorkflows(resourceResolver);
        }
    }

    /**
     * Initializes the store, creating it if necessary, and deleting any existing content if not.
     * @param resourceResolver
     * @param basePath
     * @param storeName
     * @param provider
     * @param clear true if any existing products should be cleared from the store
     * @return
     */
    protected Node setupStore(ResourceResolver resourceResolver, String basePath, String storeName, boolean clear, String provider) {
        Session session = resourceResolver.adaptTo(Session.class);

        String storePath = basePath;
        if (StringUtils.isNotEmpty(storeName)) {
            storePath += "/" + mangleName(storeName);
        }

        Resource rootResource = resourceResolver.getResource(storePath);
        Node rootNode = null;

        try {
            if (rootResource != null) {
                rootNode = rootResource.adaptTo(Node.class);

                if (clear && rootNode.hasNodes()) {
                    NodeIterator it = rootNode.getNodes();
                    while (it.hasNext()) {
                        it.nextNode().remove();
                    }
                }
            } else {
                rootNode = JcrUtil.createPath(storePath, false, "sling:Folder", "sling:Folder", session, false);
                if (StringUtils.isNotEmpty(storeName)) {
                    // In many cases mangleName() will have done serious damage to the given name (particularly if
                    // it was multi-byte); the least we can do is set the title to the correct name:
                    rootNode.setProperty(JcrConstants.JCR_TITLE, storeName);
                }
            }
            rootNode.setProperty(CommerceConstants.PN_COMMERCE_PROVIDER, provider);
            session.save();
        } catch (Exception e) {
            log.error("Failed to initialize store: ", e);
        }

        return rootNode;
    }

    /**
     * Run the actual import.  Implementation to be supplied by concrete classes.
     * @param resourceResolver
     * @param storeRoot
     * @param incrementalImport Indicates import should be applied to existing store as a delta
     * @throws RepositoryException
     * @throws IOException
     */
    protected abstract void doImport(ResourceResolver resourceResolver, Node storeRoot, boolean incrementalImport)
            throws RepositoryException, IOException;

    //
    // Performance considerations for large imports.
    //   1) only save session every SAVE_BATCH_SIZE nodes
    //   2) pause to allow observation manager to catch up every THROTTLE_BATCH_SIZE nodes
    //

    protected void openThrottle(Node storeRoot) throws  RepositoryException {
        throttle = new JcrObservationThrottle(JcrUtil.createUniqueNode(storeRoot, "temp", "nt:unstructured", storeRoot.getSession()));
        throttle.open();
    }

    protected void closeThrottle() {
        if (throttle != null) {
            throttle.close();
        }
    }

    /**
     * Should be called after each node creation.  The flush parameter can be used to force session saving and
     * event dispatch (for instance at the end of an import), otherwise saves and event dispatch are batched.
     * @param session
     * @param flush
     */
    protected void checkpoint(Session session, boolean flush) {
        saveBatchCount++;
        throttleBatchCount++;

        if (saveBatchCount > SAVE_BATCH_SIZE || flush) {
            if (StringUtils.isNotEmpty(tickerToken)) {
                try {
                    Node node = JcrUtils.getOrCreateByPath("/tmp/commerce/tickers/import_" + tickerToken, "nt:unstructured", session);
                    node.setProperty("message", tickerMessage);
                    node.setProperty("errorCount", errorCount);
                    node.setProperty("complete", tickerComplete);
                } catch (Exception e) {
                    log.error("ERROR updating ticker", e);
                }
            }

            try {
                session.save();
                saveBatchCount = 0;
            } catch (Exception e) {
                logMessage("ERROR saving session", false);  // pass false for isError as we'll bump the error count by hand
                errorCount += saveBatchCount;               // error will affect all products/variations not yet saved
                log.error("ERROR saving session", e);
            }

            if (throttleBatchCount > THROTTLE_BATCH_SIZE) {
                try {
                    long wait = throttle.waitForEvents();
                    throttleBatchCount = 0;
                } catch (RepositoryException e) {
                    // keep calm and carry on...
                }
            }
        }

        for (String eventName : eventQueues.keySet()) {
            Set paths = eventQueues.get(eventName);
            if (paths.size() > EVENT_BATCH_SIZE || (flush && paths.size() > 0)) {
                if (eventAdmin != null) {
                    Dictionary eventProperties = new Hashtable();
                    eventProperties.put("paths", paths.toArray(new String[paths.size()]));
                    eventAdmin.postEvent(new Event(eventName, eventProperties));
                }
                paths.clear();
            }
        }
    }

    //
    // Importers may also choose to disable some workflows while importing, either for performance
    // reasons or for process reasons.  Simply override the predicate and return true for those
    // workflows that should be disabled.
    //

    protected boolean disableWorkflowPredicate(ConfigEntry workflowConfigEntry) {
        return false;
    }

    protected void disableWorkflows(ResourceResolver resourceResolver) {
        try {
            WorkflowLauncher launcher = resourceResolver.adaptTo(WorkflowLauncher.class);
            Iterator entries = launcher.getConfigEntries();
            while (entries.hasNext()) {
                ConfigEntry entry = entries.next();
                if (entry.isEnabled() && disableWorkflowPredicate(entry)) {
                    entry.setEnabled(false);
                    launcher.editConfigEntry(entry.getId(), entry);
                    disabledWorkflows.add(entry.getId());
                }
            }
        } catch (WorkflowException e) {
            log.error("Error while disabling workflows", e);
        }
    }

    protected void reenableWorkflows(ResourceResolver resourceResolver) {
        try {
            WorkflowLauncher launcher = resourceResolver.adaptTo(WorkflowLauncher.class);
            Iterator entries = launcher.getConfigEntries();
            while (entries.hasNext()) {
                ConfigEntry entry = entries.next();
                if (disabledWorkflows.contains(entry.getId())) {
                    entry.setEnabled(true);
                    launcher.editConfigEntry(entry.getId(), entry);
                }
            }
        } catch (WorkflowException e) {
            log.error("Error while re-enabling workflows", e);
        }
    }

    //
    // Some utility routines.
    //

    protected static String mangleName(String name) {
        return StringUtils.isEmpty(name) ? "" : JcrUtil.createValidName(name.trim().replace(" ", "-"));
    }

    protected void createMissingTags(ResourceResolver resourceResolver, String[] tags) {
        TagManager tm = resourceResolver.adaptTo(TagManager.class);
        for (String tag : tags) {
            try {
                if (tm.canCreateTag(tag)) {
                    tm.createTag(tag, null, null, false);
                }
            } catch (InvalidTagFormatException e) {
                log.error("Invalid tag ID", e);
            }
        }
    }

    protected void logEvent(String eventName, String path) {
        Set paths = eventQueues.get(eventName);
        if (paths == null) {
            paths = new HashSet();
            eventQueues.put(eventName, paths);
        }
        paths.add(path);
    }

    protected void updateLoggedEvents(String oldPath, String newPath) {
        for (String eventName : eventQueues.keySet()) {
            Set paths = eventQueues.get(eventName);
            if (paths.remove(oldPath)) {
                paths.add(newPath);
            }
        }
    }

    protected void logMessage(String message, boolean isError) {
        if (messages.size() < MESSAGE_CAP) {
            messages.add(message);
        }
        if (isError) {
            errorCount++;
        }
    }

    protected int getErrorCount() {
        return errorCount;
    }

    protected void initTicker(String tickerToken, Session session) {
        this.tickerToken = tickerToken;
        tickerMessage = "";
        tickerComplete = false;
    }

    protected void updateTicker(String tickerMessage) {
        this.tickerMessage = tickerMessage;
    }

    protected void respondWithMessages(SlingHttpServletResponse response, String summary) throws IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");

        PrintWriter pw = response.getWriter();

        pw.println("");
        pw.println("
");
        pw.println(summary);
        if (MESSAGE_CAP > 0) {
            pw.println("");
            for (String msg : messages) {
                pw.println(msg);
            }
            if (messages.size() == MESSAGE_CAP) {
                pw.println("...");
            }
        }
        pw.println("
"); pw.println(""); pw.flush(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy