/**
* =========================================================================
* __ ____ ____ __ ____ ___ __ __ ____ ____ ____
* || || \\ || (( \ || \\ // \\ ||\ || || \\ || || \\
* || ||_// ||== \\ ||_// (( )) ||\\|| || )) ||== ||_//
* |__|| || \\ ||___ \_)) || \\_// || \|| ||_// ||___ || \\
* =========================================================================
*
* 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);
}
}
}