org.jresponder.service.SubscriberService.java Source code

Java tutorial

Introduction

Here is the source code for org.jresponder.service.SubscriberService.java

Source

/**
 * =========================================================================
 *     __ ____   ____  __  ____    ___   __  __ ____    ____ ____ 
 *     || || \\ ||    (( \ || \\  // \\  ||\ || || \\  ||    || \\
 *     || ||_// ||==   \\  ||_// ((   )) ||\\|| ||  )) ||==  ||_//
 *  |__|| || \\ ||___ \_)) ||     \\_//  || \|| ||_//  ||___ || \\
 * =========================================================================
 *
 * Copyright 2012 Brad Peabody
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * =========================================================================
 */
package org.jresponder.service;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.jresponder.dao.MainDao;
import org.jresponder.domain.LogEntryType;
import org.jresponder.domain.Subscriber;
import org.jresponder.domain.SubscriberStatus;
import org.jresponder.domain.Subscription;
import org.jresponder.domain.SubscriptionStatus;
import org.jresponder.engine.SendingEngine;
import org.jresponder.message.MessageGroup;
import org.jresponder.message.MessageGroupSource;
import org.jresponder.message.MessageRef;
import org.jresponder.util.PropUtil;
import org.jresponder.util.TokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * Perform actions related to subscribing.
 * <p>
 * TODO: we should look at how return codes are dealt with - should probably
 * make it so a certain type of exception is thrown on error, and that 
 * exception has a standard list of error types - which can correspond
 * directly to the error codes sent back via JSON-RPC 2. 
 * 
 * @author bradpeabody
 *
 */
@Service("jrSubscriberService")
public class SubscriberService {

    /* ====================================================================== */
    /* Logger boiler plate                                                    */
    /* ====================================================================== */
    private static Logger l = null;

    private Logger logger() {
        if (l == null)
            l = LoggerFactory.getLogger(this.getClass());
        return l;
    }
    /* ====================================================================== */

    @Resource(name = "jrTokenUtil")
    private TokenUtil tokenUtil;

    @Resource(name = "jrPropUtil")
    private PropUtil propUtil;

    @Resource(name = "jrMainDao")
    private MainDao mainDao;

    @Resource(name = "jrMessageGroupSource")
    private MessageGroupSource messageGroupSource;

    @Resource(name = "jrSendingEngine")
    private SendingEngine sendingEngine;

    /**
     * Default log entry props
     * @return
     */
    private static Map<String, Object> defaultLogEntryProps() {

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

        try {
            // use some Spring magic to get the current request
            HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
                    .getRequest();
            myRet.put("ip_address", req.getRemoteAddr());
        } catch (IllegalStateException e) {
            LoggerFactory.getLogger(SubscriberService.class).debug(
                    "defaultLogEntryProps() got IllegalStateException - this is normal if testing outside of web env");
        }

        return myRet;
    }

    /**
     * Subscribes someone, returns the Subscriber object that corresponds
     * 
     * @param aEmail
     * @param aSubscriberPropsMap
     * @param aMessageGroupName
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public Subscriber subscribe(String aEmail, Map<String, Object> aSubscriberPropsMap, String aMessageGroupName) {

        logger().debug("Starting subscribe()");

        try {
            // FIXME: should package these up into util calls so they are not so verbose,
            // do that before too many more of these functions are written

            // sanity checks
            if (aEmail == null || aEmail.length() < 1)
                throw new IllegalArgumentException("email cannot be null or zero length");
            // TODO: configurable?
            aEmail = aEmail.toLowerCase();

            // make sure that the message group is not empty and actually exists
            if (aMessageGroupName == null || aMessageGroupName.length() < 1)
                throw new IllegalArgumentException("messageGroupName cannot be null or zero length");
            if (messageGroupSource.getMessageGroupByName(aMessageGroupName) == null) {
                throw new IllegalArgumentException("message group does not exist");
            }

            // get the message group
            MessageGroup myMessageGroup = messageGroupSource.getMessageGroupByName(aMessageGroupName);
            logger().debug("Using MessageGroup: {}", myMessageGroup);

            // the opt-in-confirm message, if it exists
            MessageRef myOptInConfirmMessageRef = myMessageGroup.getOptInConfirmMessageRef();
            logger().debug("myOptInConfirmMessageRef: {}", myOptInConfirmMessageRef);

            // make sure we have an attribute map, even if it's empty
            if (aSubscriberPropsMap == null) {
                aSubscriberPropsMap = new HashMap<String, Object>();
            }

            logger().debug("aSubscriberPropsMap: {}", aSubscriberPropsMap);

            // find existing subscriber
            Subscriber mySubscriber = mainDao.getSubscriberByEmail(aEmail);
            logger().debug("Looked up subscriber with email ({}) and got: {}", aEmail, mySubscriber);

            Subscription mySubscription = null;

            // doesn't exist, create it
            if (mySubscriber == null) {

                logger().debug("No subscriber for this email, making a new record");

                // create subscriber
                mySubscriber = new Subscriber();
                mySubscriber.setEmail(aEmail);
                mySubscriber.setPropsMap(aSubscriberPropsMap);
                mySubscriber.setSubscriberStatus(SubscriberStatus.OK);
                mainDao.persist(mySubscriber);

                logger().debug("Now making a new subscription record");

                // create subscription
                mySubscription = new Subscription(mySubscriber, aMessageGroupName);
                mySubscription.setNextSendDate(new Date());
                mySubscription.setToken(tokenUtil.generateToken());

                // send opt in confirm message if applicable
                if (myOptInConfirmMessageRef != null) {
                    logger().debug("About to send opt-in confirm message...");
                    doOptInConfirmMessage(myOptInConfirmMessageRef, myMessageGroup, mySubscriber, mySubscription);
                }
                // if no opt in, then just mark active
                else {
                    logger().debug("No opt-in confirm message, just marking as active");
                    mySubscription.setSubscriptionStatus(SubscriptionStatus.ACTIVE);
                }

                logger().debug("Saving subscription");
                mainDao.persist(mySubscription);

                /* ========================================================== */
                /* Make LogEntry                                              */
                /* ========================================================== */
                mainDao.logEntry(LogEntryType.SUBSCRIBED, mySubscriber, aMessageGroupName, defaultLogEntryProps());

            }

            // already there, merge
            else {

                logger().debug("Already have a Subscriber object for email address ({}): {}", aEmail, mySubscriber);

                // update attributes
                mySubscriber.setPropsMap((Map<String, Object>) PropUtil.getInstance()
                        .propMerge(mySubscriber.getPropsMap(), aSubscriberPropsMap));

                if (logger().isDebugEnabled()) {
                    logger().debug("Saving updated properties: {}", mySubscriber.getPropsMap());
                }

                mainDao.persist(mySubscriber);

                // see if the subscription is there
                mySubscription = mainDao.getSubscriptionBySubscriberAndMessageGroupName(mySubscriber,
                        myMessageGroup.getName());

                logger().debug(
                        "Looking for corresponding Subscription record for subscriber and message group ({}) found: {}",
                        myMessageGroup.getName(), mySubscription);

                // no subscription, create it
                if (mySubscription == null) {

                    mySubscription = new Subscription(mySubscriber, aMessageGroupName);
                    mySubscription.setNextSendDate(new Date());
                    mySubscription.setToken(tokenUtil.generateToken());

                    // send opt in confirm message if applicable
                    if (myOptInConfirmMessageRef != null) {
                        logger().debug("About to send opt-in confirm message...");
                        doOptInConfirmMessage(myOptInConfirmMessageRef, myMessageGroup, mySubscriber,
                                mySubscription);
                    }
                    // if no opt in, then just mark active
                    else {
                        logger().debug("No opt-in confirm message, just marking as active");
                        mySubscription.setSubscriptionStatus(SubscriptionStatus.ACTIVE);
                    }

                    logger().debug("Saving subscription");
                    mainDao.persist(mySubscription);

                    /* ========================================================== */
                    /* Make LogEntry                                              */
                    /* ========================================================== */
                    mainDao.logEntry(LogEntryType.SUBSCRIBED, mySubscriber, aMessageGroupName,
                            defaultLogEntryProps());
                }

                // we do already have a subscription
                else {

                    // see if it's active
                    if (mySubscription.getSubscriptionStatus() != SubscriptionStatus.ACTIVE) {

                        // send opt in confirm message if applicable
                        if (myOptInConfirmMessageRef != null) {
                            doOptInConfirmMessage(myOptInConfirmMessageRef, myMessageGroup, mySubscriber,
                                    mySubscription);
                        }
                        // if no opt in, then just mark active
                        else {
                            mySubscription.setSubscriptionStatus(SubscriptionStatus.ACTIVE);
                        }

                    }

                    // save subscription
                    mainDao.persist(mySubscription);

                    /* ========================================================== */
                    /* Make LogEntry                                              */
                    /* ========================================================== */
                    mainDao.logEntry(LogEntryType.RESUBSCRIBED, mySubscriber, aMessageGroupName,
                            defaultLogEntryProps());
                }

            }

            // now that we've done all the work -
            // re-read subscriber, so we get the latest (probably not
            // necessary, but it makes it me feel better ;)
            mySubscriber = mainDao.getSubscriberById(mySubscriber.getId());

            // log an info, just for politeness
            logger().info("User '{}' subscribed to message group '{}' ({} opt-in confirm)",
                    new Object[] { mySubscriber.getEmail(), mySubscription.getMessageGroupName(),
                            (myOptInConfirmMessageRef != null ? "with" : "without") });

            return mySubscriber;

        } catch (Throwable t) {
            throw new RuntimeException(t);
        }

    }

    /**
     * Unsubscribe from token.  Sets this individual Subscription as inactive.
     * 
     * @param aToken
     * @return true if found and marked, false if not found 
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean unsubscribeFromToken(String aToken) {

        Subscription mySubscription = mainDao.getSubscriptionByToken(aToken);
        if (mySubscription == null) {
            return false;
        }

        mySubscription.setSubscriptionStatus(SubscriptionStatus.INACTIVE);
        mySubscription.setNextSendDate(null);

        mainDao.persist(mySubscription);

        Subscriber mySubscriber = mySubscription.getSubscriber();
        if (mySubscriber == null) {
            throw new IllegalStateException(
                    "Subscription record found but no corresponding subscriber record - this is an internal database error");
        }

        logger().info(
                "Subscription corresponding to (email={} messageGroupName={}) was marked as INACTIVE (this particular email/message group combination is now not active)",
                mySubscriber.getEmail(), mySubscription.getMessageGroupName());

        return true;
    }

    /**
     * Sets the subscriber status to REMOVE, meaning he won't
     * get any more mail at all from this system.
     * 
     * @param aToken
     * @return true if found and marked, false if not found 
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean removeFromToken(String aToken) {

        Subscription mySubscription = mainDao.getSubscriptionByToken(aToken);
        if (mySubscription == null) {
            return false;
        }

        Subscriber mySubscriber = mySubscription.getSubscriber();
        if (mySubscriber == null) {
            throw new IllegalStateException(
                    "Subscription record found but no corresponding subscriber record - this is an internal database error");
        }

        mySubscriber.setSubscriberStatus(SubscriberStatus.REMOVED);
        mainDao.persist(mySubscriber);

        logger().info("Subscriber with email ({}) was REMOVED (no more emails from an message groups)",
                mySubscriber.getEmail());

        return true;

    }

    /**
     * Performs an opt-in confirmation from a token.
     * 
     * @param aToken
     * @return true if found and confirmed, false if not found or could
     *         not confirm due to subscriber or subscription being in
     *         funky state.
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void confirmFromToken(String aToken) throws ServiceException {

        Subscription mySubscription = mainDao.getSubscriptionByToken(aToken);
        if (mySubscription == null) {
            throw new ServiceException(ServiceExceptionType.NO_SUCH_SUBSCRIPTION,
                    propUtil.mkprops("token", aToken));
        }

        Subscriber mySubscriber = mySubscription.getSubscriber();
        if (mySubscriber == null) {
            throw new ServiceException(ServiceExceptionType.NO_SUCH_SUBSCRIBER,
                    propUtil.mkprops("token", aToken, "jr_subscription_id", mySubscription.getId()));
        }

        logger().debug("Attempting to confirm subscription (token={}) for subscriber (email={})",
                new Object[] { mySubscription.getToken(), mySubscriber.getEmail() });

        // make sure subscriber is marked as OK
        if (mySubscriber.getSubscriberStatus() != SubscriberStatus.OK) {
            logger().debug("Could not confirm, subscriber status was not OK, instead it was: {}",
                    mySubscriber.getSubscriberStatus());
            throw new ServiceException(ServiceExceptionType.SUBSCRIBER_NOT_OK, propUtil.mkprops("token", aToken));
        }

        // make sure subscription is marked as confirm wait or is already active
        if (!(mySubscription.getSubscriptionStatus() == SubscriptionStatus.CONFIRM_WAIT
                || mySubscription.getSubscriptionStatus() == SubscriptionStatus.ACTIVE)) {
            logger().debug(
                    "Could not confirm, subscription status was not CONFIRM_WAIT or ACTIVE, instead it was: {}",
                    mySubscription.getSubscriptionStatus());
            throw new ServiceException(ServiceExceptionType.UNEXPECTED_SUBSCRIPTION_STATUS,
                    propUtil.mkprops("token", aToken));
        }

        mySubscription.setSubscriptionStatus(SubscriptionStatus.ACTIVE);
        // if next send date is null, then set it to now - so it goes to the
        // engine to be scheduled properly
        if (mySubscription.getNextSendDate() == null) {
            mySubscription.setNextSendDate(new Date());
        }
        mainDao.persist(mySubscription);

        logger().info("Subscription (email={},message_group_name={},token={}) was confirmed!", new Object[] {
                mySubscriber.getEmail(), mySubscription.getMessageGroupName(), mySubscription.getToken() });

    }

    /**
     * Send an opt-in confirmation message
     * 
     * @param aOptInConfirmMessageRef
     * @param aSubscriber
     * @param aSubscription
     */
    @Transactional(propagation = Propagation.REQUIRED)
    protected void doOptInConfirmMessage(MessageRef aOptInConfirmMessageRef, MessageGroup aMessageGroup,
            Subscriber aSubscriber, Subscription aSubscription) {

        // send message
        if (sendingEngine.sendMessage(aOptInConfirmMessageRef, aMessageGroup, aSubscriber, aSubscription,
                LogEntryType.MESSAGE_SENT)) {
            logger().debug("Sent opt-in confirm message");
            aSubscription.setSubscriptionStatus(SubscriptionStatus.CONFIRM_WAIT);
        }

        // if opt-in message skipped then we mark as active right away
        else {
            logger().debug("Opt-in confirm message was skipped (due to no body)");
            aSubscription.setSubscriptionStatus(SubscriptionStatus.ACTIVE);
        }

    }

    /**
     * Gets a subscriber and all of his Subscriptions
     * 
     * @param aEmail
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRED, readOnly = true)
    public Subscriber lookupSubscriber(String aEmail) {

        try {

            if (aEmail == null) {
                return null;
            }

            aEmail = aEmail.toLowerCase();

            // find existing subscriber
            Subscriber mySubscriber = mainDao.getSubscriberByEmail(aEmail);

            // force Hibernate to populate the Subscriptions list
            if (mySubscriber != null) {
                mySubscriber.getSubscriptions();
            }

            return mySubscriber;

        } catch (Throwable t) {
            throw new RuntimeException(t);
        }

    }

}