org.jahia.modules.newsletter.service.SubscriptionService.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.modules.newsletter.service.SubscriptionService.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *     Copyright (C) 2002-2015 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ======================================================================================
 *
 *     IF YOU DECIDE TO CHOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     "This program is free software; you can redistribute it and/or
 *     modify it under the terms of the GNU General Public License
 *     as published by the Free Software Foundation; either version 2
 *     of the License, or (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program; if not, write to the Free Software
 *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 *     As a special exception to the terms and conditions of version 2.0 of
 *     the GPL (or any later version), you may redistribute this Program in connection
 *     with Free/Libre and Open Source Software ("FLOSS") applications as described
 *     in Jahia's FLOSS exception. You should have received a copy of the text
 *     describing the FLOSS exception, also available here:
 *     http://www.jahia.com/license"
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ======================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 *
 *
 * ==========================================================================================
 * =                                   ABOUT JAHIA                                          =
 * ==========================================================================================
 *
 *     Rooted in Open Source CMS, Jahias Digital Industrialization paradigm is about
 *     streamlining Enterprise digital projects across channels to truly control
 *     time-to-market and TCO, project after project.
 *     Putting an end to the Tunnel effect?, the Jahia Studio enables IT and
 *     marketing teams to collaboratively and iteratively build cutting-edge
 *     online business solutions.
 *     These, in turn, are securely and easily deployed as modules and apps,
 *     reusable across any digital projects, thanks to the Jahia Private App Store Software.
 *     Each solution provided by Jahia stems from this overarching vision:
 *     Digital Factory, Workspace Factory, Portal Factory and eCommerce Factory.
 *     Founded in 2002 and headquartered in Geneva, Switzerland,
 *     Jahia Solutions Group has its North American headquarters in Washington DC,
 *     with offices in Chicago, Toronto and throughout Europe.
 *     Jahia counts hundreds of global brands and governmental organizations
 *     among its loyal customers, in more than 20 countries across the globe.
 *
 *     For more information, please visit http://www.jahia.com
 */
package org.jahia.modules.newsletter.service;

import au.com.bytecode.opencsv.CSVReader;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.jahia.modules.newsletter.service.model.Subscription;
import org.jahia.services.content.JCRContentUtils;
import org.jahia.services.content.JCRNodeWrapper;
import org.jahia.services.content.JCRPropertyWrapper;
import org.jahia.services.content.JCRSessionWrapper;
import org.jahia.services.content.decorator.JCRUserNode;
import org.jahia.services.usermanager.JahiaUser;
import org.jahia.services.usermanager.JahiaUserManagerService;
import org.jahia.utils.PaginatedList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;

import javax.jcr.*;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Jahia subscription service implementation.
 *
 * @author Sergiy Shyrkov
 */
public class SubscriptionService {
    private static final String J_ALLOW_UNREGISTERED_USERS = "j:allowUnregisteredUsers";
    public static final String J_CONFIRMATION_KEY = "j:confirmationKey";
    public static final String J_CONFIRMED = "j:confirmed";
    private static final String J_EMAIL = "j:email";
    private static final String J_FIRST_NAME = "j:firstName";
    private static final String J_LAST_NAME = "j:lastName";
    private static final String J_SUBSCRIBER = "j:subscriber";
    private static final String J_SUBSCRIPTIONS = "j:subscriptions";
    private static final String J_SUSPENDED = "j:suspended";
    private static final String JMIX_SUBSCRIBABLE = "jmix:subscribable";
    private static final String JNT_SUBSCRIPTION = "jnt:subscription";

    private static final Logger logger = LoggerFactory.getLogger(SubscriptionService.class);

    @Autowired
    private JahiaUserManagerService userManagerService;

    /**
     * Cancels the specified subscriptions.
     *
     * @param subscriptionIds the list of IDs for subscriptions being canceled
     * @param session
     */
    public void cancel(final List<String> subscriptionIds, JCRSessionWrapper session) {
        try {
            int count = 0;
            for (String id : subscriptionIds) {
                try {
                    JCRNodeWrapper target = session.getNodeByIdentifier(id);
                    count++;
                    session.checkout(target.getParent());
                    target.remove();
                } catch (ItemNotFoundException e) {
                    logger.warn("Unable to find subscription node for identifier {}. Skip cancelling subscription.",
                            id);
                }
            }
            if (count > 0) {
                session.save();
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Successfully cancelled {} subscriptions.", count);
            }
        } catch (RepositoryException e) {
            logger.error("Error cancelling subscriptions " + subscriptionIds, e);
        }
    }

    /**
     * Cancels the specified subscription.
     *
     * @param subscriptionId the IDs of a subscription being canceled
     * @param session
     */
    public void cancel(String subscriptionId, JCRSessionWrapper session) {
        List<String> ids = new LinkedList<String>();
        ids.add(subscriptionId);

        cancel(ids, session);
    }

    /**
     * Changes the suspended status of the specified subscriptions.
     *
     * @param subscriptionIds
     * @param session
     */
    private void changeSuspendedStatus(final List<String> subscriptionIds, final boolean doSuspend,
            JCRSessionWrapper session) {
        try {
            int count = 0;
            for (String subscriptionId : subscriptionIds) {
                try {
                    JCRNodeWrapper subscriptionNode = session.getNodeByIdentifier(subscriptionId);
                    JCRPropertyWrapper property = subscriptionNode.getProperty(J_SUSPENDED);
                    if (doSuspend && !property.getBoolean() || !doSuspend && property.getBoolean()) {
                        count++;
                        session.checkout(subscriptionNode);
                        subscriptionNode.setProperty(J_SUSPENDED, Boolean.valueOf(doSuspend));
                    }
                } catch (ItemNotFoundException nfe) {
                    logger.warn("Unable to find subscription node for identifier {}", subscriptionId);
                } catch (RepositoryException e) {
                    logger.error("Error changing suspended status of the subscription with ID " + subscriptionId,
                            e);
                }
                if (count > 0) {
                    session.save();
                }
            }
        } catch (RepositoryException e) {
            logger.error("Error changing suspended status of subscriptions " + subscriptionIds, e);
        }
    }

    /**
     * Retrieves subscription for the specified node.
     *
     * @param subscribableIdentifier the UUID of the subscribable node
     * @param orderBy                order by property; <code>null</code> if no sorting should be
     *                               done
     * @param orderAscending         do we sort in ascending direction?
     * @param offset                 the index of the first result to start with; <code>0</code> to
     *                               start from the beginning
     * @param limit                  the maximum number of results to return; <code>0</code> to
     *                               return all available
     * @param session
     * @return paginated list list of {@link org.jahia.services.notification.Subscription} objects
     */
    public PaginatedList<Subscription> getSubscriptions(final String subscribableIdentifier, final String orderBy,
            final boolean orderAscending, final int offset, final int limit, JCRSessionWrapper session) {
        return getSubscriptions(subscribableIdentifier, false, false, orderBy, orderAscending, offset, limit,
                session);
    }

    /**
     * Retrieves subscription for the specified node.
     *
     * @param subscribableIdentifier the UUID of the subscribable node
     * @param activeOnly          return only non-suspended subscriptions
     * @param confirmedOnly          return only confirmed subscriptions
     * @param orderBy                order by property; <code>null</code> if no sorting should be
     *                               done
     * @param orderAscending         do we sort in ascending direction?
     * @param offset                 the index of the first result to start with; <code>0</code> to
     *                               start from the beginning
     * @param limit                  the maximum number of results to return; <code>0</code> to
     *                               return all available
     * @param session
     * @return paginated list list of {@link org.jahia.services.notification.Subscription} objects
     */
    public PaginatedList<Subscription> getSubscriptions(final String subscribableIdentifier,
            final boolean activeOnly, final boolean confirmedOnly, final String orderBy,
            final boolean orderAscending, final int offset, final int limit, JCRSessionWrapper session) {

        long timer = System.currentTimeMillis();

        int total = 0;
        final List<Subscription> subscriptions = new LinkedList<Subscription>();
        try {
            JCRNodeWrapper target = session.getNodeByIdentifier(subscribableIdentifier);
            if (!target.isNodeType(JMIX_SUBSCRIBABLE)) {
                logger.warn("The target node {} does not have the " + JMIX_SUBSCRIBABLE + " mixin."
                        + " No subscriptions can be found.", target.getPath());
                return new PaginatedList<Subscription>(subscriptions, 0);
            }
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            if (queryManager == null) {
                logger.error("Unable to obtain QueryManager instance");
                return new PaginatedList<Subscription>(subscriptions, 0);
            }

            StringBuilder q = new StringBuilder();
            q.append("select * from [" + JNT_SUBSCRIPTION + "] where isdescendantnode([").append(target.getPath())
                    .append("/" + J_SUBSCRIPTIONS + "])");
            if (activeOnly) {
                q.append(" and [" + J_SUSPENDED + "]=false");
            }
            if (confirmedOnly) {
                q.append(" and [" + J_CONFIRMED + "]=true");
            }
            if (orderBy != null) {
                q.append(" order by [").append(orderBy).append("]").append(orderAscending ? "asc" : "desc");
            }
            Query query = queryManager.createQuery(q.toString(), Query.JCR_SQL2);

            if (limit > 0 || offset > 0) {
                total = (int) JCRContentUtils.size(query.execute().getNodes());
            }

            query.setLimit(limit);
            query.setOffset(offset);

            for (NodeIterator nodes = query.execute().getNodes(); nodes.hasNext();) {
                JCRNodeWrapper subscriptionNode = (JCRNodeWrapper) nodes.next();
                subscriptions.add(toSubscription(subscriptionNode, session));
            }
        } catch (RepositoryException e) {
            logger.error("Error retrieving subscriptions for node " + subscribableIdentifier, e);
        }

        if (logger.isDebugEnabled()) {
            logger.info("Subscriber search took " + (System.currentTimeMillis() - timer) + " ms. Returning "
                    + subscriptions.size() + " subscriber(s)");
        }

        return new PaginatedList<Subscription>(subscriptions,
                limit > 0 || offset > 0 ? total : subscriptions.size());
    }

    /**
     * Checks if the provided user is subscribed to the specified node.
     *
     * @param subscribableIdentifier the UUID of the target subscribable node
     * @param user                   the user key for the registered users or an e-mail for
     *                               non-registered users
     * @param session
     * @return <code>true</code> if the provided user is subscribed to the
     *         specified node
     */
    public String getSubscription(final String subscribableIdentifier, final String user,
            JCRSessionWrapper session) {
        try {
            JCRNodeWrapper target = session.getNodeByIdentifier(subscribableIdentifier);
            if (!target.isNodeType(JMIX_SUBSCRIBABLE)) {
                logger.warn("The target node {} does not have the " + JMIX_SUBSCRIBABLE + " mixin."
                        + " No subscriptions can be found.", target.getPath());
                return null;
            }

            JCRNodeWrapper sub = getSubscription(target, user, session);
            if (sub != null) {
                return sub.getIdentifier();
            }
            return null;
        } catch (RepositoryException e) {
            logger.error(
                    "Error checking subscription status for user '" + user + "' and node " + subscribableIdentifier,
                    e);
        }
        return null;
    }

    /**
     * Checks if the provided user is subscribed to the specified node.
     *
     * @param target  the path of the target subscribable node
     * @param user    the user key for the registered users or an e-mail for
     *                non-registered users
     * @param session the JCR session
     * @return <code>true</code> if the provided user is subscribed to the
     *         specified node
     * @throws javax.jcr.RepositoryException   in case of a JCR error
     * @throws javax.jcr.query.InvalidQueryException if the query syntax is invalid
     */
    public JCRNodeWrapper getSubscription(JCRNodeWrapper target, String user, JCRSessionWrapper session)
            throws InvalidQueryException, RepositoryException {
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        if (queryManager == null) {
            logger.error("Unable to obtain QueryManager instance");
            return null;
        }
        StringBuilder q = new StringBuilder(64);
        q.append("select * from [" + JNT_SUBSCRIPTION + "] where [" + J_SUBSCRIBER + "]='").append(user)
                .append("'");
        q.append(" and").append(" isdescendantnode([").append(target.getPath()).append("])");
        Query query = queryManager.createQuery(q.toString(), Query.JCR_SQL2);
        query.setLimit(1);
        final NodeIterator nodeIterator = query.execute().getNodes();
        if (nodeIterator.hasNext()) {
            return (JCRNodeWrapper) nodeIterator.nextNode();
        }
        return null;
    }

    /**
     * Resumes the specified subscriptions.
     *
     * @param subscriptionIds the list of subscription IDs to be resumed
     * @param session
     */
    public void resume(List<String> subscriptionIds, JCRSessionWrapper session) {
        changeSuspendedStatus(subscriptionIds, false, session);
    }

    /**
     * Resumes the specified subscription.
     *
     * @param subscriptionId the subscription ID to be resumed
     * @param session
     */
    public void resume(final String subscriptionId, JCRSessionWrapper session) {
        List<String> subscriptions = new LinkedList<String>();
        subscriptions.add(subscriptionId);
        changeSuspendedStatus(subscriptions, false, session);
    }

    public String generateConfirmationKey(JCRNodeWrapper subscription) {
        try {
            return DigestUtils.md5Hex(subscription.getIdentifier() + System.currentTimeMillis());
        } catch (RepositoryException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    public JCRNodeWrapper getSubscriptionFromKey(String key, JCRSessionWrapper session) {
        try {
            Query q = session.getWorkspace().getQueryManager().createQuery(
                    "select * from [" + JNT_SUBSCRIPTION + "] where [" + J_CONFIRMATION_KEY + "]='" + key + "'",
                    Query.JCR_SQL2);
            NodeIterator ni = q.execute().getNodes();
            if (ni.hasNext()) {
                return (JCRNodeWrapper) ni.nextNode();
            }
        } catch (RepositoryException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    public void setUserManagerService(JahiaUserManagerService userManagerService) {
        this.userManagerService = userManagerService;
    }

    protected void storeProperties(JCRNodeWrapper newSubscriptionNode, Map<String, Object> properties,
            JCRSessionWrapper session) {
        if (properties == null || properties.isEmpty()) {
            return;
        }

        for (Map.Entry<String, Object> property : properties.entrySet()) {
            if (!property.getValue().getClass().isArray()) {
                try {
                    newSubscriptionNode.setProperty(property.getKey(),
                            JCRContentUtils.createValue(property.getValue(), session.getValueFactory()));
                } catch (RepositoryException e) {
                    logger.warn("Unable to set property " + property.getKey(), e);
                }
            } else {
                logger.warn("Cannot handle nultivalue properties. Skipping property {}", property.getKey());
            }
        }
    }

    /**
     * Import the subscriber data from the specified CSV file and creates
     * subscriptions for the provided subscribable node.
     *
     * @param subscribableIdentifier the UUID of the target subscribable node
     * @param subscribersCSVFile     the subscribers file in CSV format
     * @param session
     */
    public void importSubscriptions(String subscribableIdentifier, MultipartFile subscribersCSVFile,
            JCRSessionWrapper session, char separator) {
        long timer = System.currentTimeMillis();

        if (logger.isDebugEnabled()) {
            logger.debug("Start importing subscriptions for source node {}", subscribableIdentifier);
        }

        int importedCount = 0;

        InputStream is = null;
        CSVReader reader = null;
        try {
            is = new BufferedInputStream(subscribersCSVFile.getInputStream());
            reader = new CSVReader(new InputStreamReader(is, "UTF-8"), separator);
            String[] columns = reader.readNext();
            int usernamePosition = ArrayUtils.indexOf(columns, "j:nodename");
            int emailPosition = ArrayUtils.indexOf(columns, J_EMAIL);
            if (usernamePosition == -1 && emailPosition == -1) {
                logger.warn("No data for importing subscriptions is found" + " or the file is not well-formed");
                return;
            }
            Map<String, Map<String, Object>> subscribers = new HashMap<String, Map<String, Object>>();
            String[] nextLine;
            while ((nextLine = reader.readNext()) != null) {
                String userKey = usernamePosition != -1 ? nextLine[usernamePosition] : null;
                String email = emailPosition != -1 ? nextLine[emailPosition] : null;
                boolean registered = true;
                if (StringUtils.isNotEmpty(userKey)) {
                    // registered Jahia user is provided
                    JCRUserNode user = userKey.charAt(0) == '/' ? userManagerService.lookupUserByPath(userKey)
                            : userManagerService.lookupUser(userKey);
                    if (user != null) {
                        userKey = user.getPath();
                    } else {
                        logger.warn("No user can be found for the specified username '" + userKey
                                + "'. Skipping subscription: " + StringUtils.join(nextLine, separator));
                        continue;
                    }
                } else if (StringUtils.isNotEmpty(email)) {
                    userKey = email;
                    registered = false;
                } else {
                    logger.warn("Neither a j:nodename nor j:email is provided." + "Skipping subscription: "
                            + StringUtils.join(nextLine, separator));
                    continue;
                }
                Map<String, Object> props = new HashMap<String, Object>(columns.length);
                for (int i = 0; i < columns.length; i++) {
                    String column = columns[i];
                    if ("j:nodename".equals(column) || !registered && J_EMAIL.equals(column)) {
                        continue;
                    }
                    props.put(column, nextLine[i]);
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Subscribing '" + userKey + "' with properties: " + props);
                }

                subscribers.put(userKey, props);
                if (subscribers.size() > 1000) {
                    // flush
                    subscribe(subscribableIdentifier, subscribers, session);
                    importedCount += subscribers.size();
                    subscribers = new HashMap<String, Map<String, Object>>();
                }
            }
            if (!subscribers.isEmpty()) {
                // subscribe the rest
                importedCount += subscribers.size();
                subscribe(subscribableIdentifier, subscribers, session);
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception e) {
                    // ignore
                }
            }
            IOUtils.closeQuietly(is);
        }

        if (logger.isInfoEnabled()) {
            logger.info("Importing {} subscriptions for source node {} took {} ms",
                    new Object[] { importedCount, subscribableIdentifier, System.currentTimeMillis() - timer });
        }
    }

    /**
     * Creates subscription for the specified users and subscribable node.
     *
     * @param subscribableIdentifier the UUID of the target subscribable node
     * @param userKeys               the list of values for registered Jahia user keys (the one,
     *                               returned by {@link org.jahia.services.usermanager.JahiaUser#getUserKey()})
     * @param session
     */
    public JCRNodeWrapper subscribe(final String subscribableIdentifier, List<String> userKeys,
            JCRSessionWrapper session) {
        Map<String, Map<String, Object>> subscribers = new HashMap<String, Map<String, Object>>(userKeys.size());
        for (String user : userKeys) {
            subscribers.put(user, null);
        }
        return subscribe(subscribableIdentifier, subscribers, session);
    }

    /**
     * Creates subscription for the specified users and subscribable node
     *
     * @param subscribableIdentifier the UUID of the target subscribable node
     * @param subscribers            a map with subscriber information. The key is a subscriber ID,
     *                               the value is a map with additional properties that will be
     *                               stored for the subscription object. The subscriber ID is a a
     *                               user key in case of a registered Jahia user (the one, returned
     *                               by {@link org.jahia.services.usermanager.JahiaUser#getUserKey()}). In case of a
     *                               non-registered user this is an e-mail address of the
     * @param session
     */
    public JCRNodeWrapper subscribe(final String subscribableIdentifier,
            final Map<String, Map<String, Object>> subscribers, JCRSessionWrapper session) {

        JCRNodeWrapper newSubscriptionNode = null;
        try {
            JCRNodeWrapper target = session.getNodeByIdentifier(subscribableIdentifier);
            if (!target.isNodeType(JMIX_SUBSCRIBABLE)) {
                logger.warn("The target node {} does not have the " + JMIX_SUBSCRIBABLE + " mixin."
                        + " No subscriptions can be created.", target.getPath());
                return null;
            }
            JCRNodeWrapper subscriptionsNode = target.getNode(J_SUBSCRIPTIONS);
            String targetPath = subscriptionsNode.getPath();
            if (target.isLocked() || subscriptionsNode.isLocked()) {
                logger.info("The target {} is locked and no subscriptions can be created. Skipping {} subscribers.",
                        targetPath, subscribers.size());
            }

            boolean allowUnregisteredUsers = target.hasProperty(J_ALLOW_UNREGISTERED_USERS)
                    ? target.getProperty(J_ALLOW_UNREGISTERED_USERS).getBoolean()
                    : true;

            for (Map.Entry<String, Map<String, Object>> subscriber : subscribers.entrySet()) {
                String username = subscriber.getKey();
                if (username.charAt(0) != '/' && !allowUnregisteredUsers) {
                    logger.info(
                            "The target {} does not allow unregistered users to be subscribed. Skipping subscription for {}.",
                            targetPath, subscriber.getKey());
                    continue;
                }

                if (getSubscription(target, subscriber.getKey(), session) == null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Creating subscription to the {} for {}.", targetPath, subscriber.getKey());
                    }
                    session.checkout(subscriptionsNode);
                    newSubscriptionNode = subscriptionsNode.addNode(
                            JCRContentUtils.findAvailableNodeName(subscriptionsNode, "subscription"),
                            JNT_SUBSCRIPTION);
                    newSubscriptionNode.setProperty(J_SUBSCRIBER, username);
                    storeProperties(newSubscriptionNode, subscriber.getValue(), session);
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug(
                                "Subscription for the {} and {} is already present. Skipping ceraring new one.",
                                targetPath, subscriber.getKey());
                    }
                }
            }
            session.save();
        } catch (RepositoryException e) {
            logger.error("Error creating subscriptions for node " + subscribableIdentifier, e);
        }
        return newSubscriptionNode;
    }

    /**
     * Creates subscription for the specified user and subscribable node
     *
     * @param subscribableIdentifier the UUID of the target subscribable node
     * @param userKey                the key of a registered Jahia user (the one, returned by
     *                               {@link org.jahia.services.usermanager.JahiaUser#getUserKey()})
     * @param session
     */
    public JCRNodeWrapper subscribe(final String subscribableIdentifier, String userKey, boolean confirmationNeeded,
            JCRSessionWrapper session) {
        Map<String, Map<String, Object>> subscribers = new HashMap<String, Map<String, Object>>(1);
        Map<String, Object> props = new HashMap<String, Object>(1);
        props.put(J_CONFIRMED, Boolean.valueOf(!confirmationNeeded));
        subscribers.put(userKey, props);

        return subscribe(subscribableIdentifier, subscribers, session);
    }

    /**
     * Creates subscription for the specified user and subscribable node
     *
     * @param subscribableIdentifier the UUID of the target subscribable node
     * @param subscriberEmail        an e-mail for the non-registered user to be subscribed
     * @param properties             additional properties to be stored for the subscription (e.g.
     * @param session
     */
    public JCRNodeWrapper subscribe(final String subscribableIdentifier, String subscriberEmail,
            Map<String, Object> properties, JCRSessionWrapper session) {
        Map<String, Map<String, Object>> subscribers = new HashMap<String, Map<String, Object>>(1);
        subscribers.put(subscriberEmail, properties);
        return subscribe(subscribableIdentifier, subscribers, session);
    }

    /**
     * Suspends corresponding subscriptions.
     *
     * @param session
     */
    public void suspend(List<String> subscriptionIds, JCRSessionWrapper session) {
        changeSuspendedStatus(subscriptionIds, true, session);
    }

    /**
     * Suspends the specified subscription.
     *
     * @param subscriptionId the subscription ID to be suspended
     * @param session
     */
    public void suspend(final String subscriptionId, JCRSessionWrapper session) {
        List<String> subscriptions = new LinkedList<String>();
        subscriptions.add(subscriptionId);
        changeSuspendedStatus(subscriptions, true, session);
    }

    protected Subscription toSubscription(JCRNodeWrapper subscriptionNode, JCRSessionWrapper session)
            throws ValueFormatException, PathNotFoundException, RepositoryException {
        Subscription subscriber = new Subscription();

        subscriber.setId(subscriptionNode.getIdentifier());
        String subscriberKey = subscriptionNode.getPropertyAsString(J_SUBSCRIBER);
        subscriber.setSubscriber(subscriberKey);

        if (subscriberKey != null && subscriberKey.charAt(0) == '/') {
            // registered user
            JCRUserNode user = userManagerService.lookupUserByPath(subscriberKey);
            if (user != null) {
                subscriber.setFirstName(user.getPropertyAsString(J_FIRST_NAME));
                subscriber.setLastName(user.getPropertyAsString(J_LAST_NAME));
                subscriber.setEmail(user.getPropertyAsString(J_EMAIL));
                subscriber.setName(user.getName());
                subscriber.setRegisteredUser(true);
            } else {
                logger.warn("Unable to find user for key {}", subscriberKey);
            }
        } else {
            subscriber.setEmail(subscriberKey);
            try {
                subscriber.setFirstName(subscriptionNode.getProperty(J_FIRST_NAME).getString());
            } catch (PathNotFoundException e) {
                // no property set
            }
            try {
                subscriber.setLastName(subscriptionNode.getProperty(J_LAST_NAME).getString());
            } catch (PathNotFoundException e) {
                // no property set
            }
        }

        try {
            subscriber.setConfirmationKey(subscriptionNode.getProperty(J_CONFIRMATION_KEY).getString());
        } catch (PathNotFoundException e) {
            // no confirmation key set
        }
        subscriber.setConfirmed(subscriptionNode.getProperty(J_CONFIRMED).getBoolean());
        subscriber.setSuspended(subscriptionNode.getProperty(J_SUSPENDED).getBoolean());

        subscriber.getProperties().putAll(subscriptionNode.getPropertiesAsString());

        return subscriber;
    }

}