Package org.jresponder.engine

Source Code of org.jresponder.engine.SendingEngineImpl

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

import java.util.Date;
import java.util.List;

import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;

import org.joda.time.DateTime;
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.message.MessageGroup;
import org.jresponder.message.MessageGroupSource;
import org.jresponder.message.MessageRef;
import org.jresponder.util.PropUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
* Gets called on a timer to poll for new messages to send, and if found
* then sends them.  Has low overhead unless messages need to be sent, at
* which it comes to life and burns through whatever messages are now
* ready in order to catch up.
*
* @author bradpeabody
*/
@Component("jrSendingEngine")
public class SendingEngineImpl implements SendingEngine, BeanFactoryAware {
 
  /* ====================================================================== */
  /* Logger boiler plate                                                    */
  /* ====================================================================== */
  private static Logger l = null;
  private Logger logger() { if (l == null) l = LoggerFactory.getLogger(this.getClass()); return l; }
  /* ====================================================================== */
 
  /**
   * How many milliseconds do we intend our main loop to take.
   */
  private static final long TARGET_LOOP_TIME = 15000;
 
  /**
   * How many milliseconds wait should there be between loops that
   * occur back to back (i.e. when the loop has work to do the entire
   * time). This is approximate, and the wait will usually be a bit less
   * than this.
   */
  private static final long LOOP_WAIT = 100;
 

  @Resource(name="jrJavaMailSender")
  private JavaMailSender javaMailSender;
 
//  @Resource(name="jrEntityManagerFactory")
//    private EntityManagerFactory entityManagerFactory;
 
  @Resource(name="jrMessageGroupSource")
  private MessageGroupSource messageGroupSource;
 
  @Resource(name="jrSendConfigGroup")
  private SendConfigGroup sendConfigGroup;
 
  @Resource(name="jrMainDao")
  private MainDao mainDao;
 
  @Resource(name="jrPropUtil")
  private PropUtil propUtil;

  /** the Spring context */
  private BeanFactory beanFactory;
 
  private volatile boolean engineLoopActive = false;
 
  /**
   * Listen for our context.  We need this as part of the main loop
   * to be able to do method calls via our AOP proxy and thus allow
   * the transaction manager to do it's thing.  See the source of
   * {@link #engineLoop()}.
   */
  @Override
  public void setBeanFactory(BeanFactory aBeanFactory) {
    beanFactory = aBeanFactory;
  }

  /**
   * Called to poll the overall state and examine what needs to be sent.
   * If nothing needs to be sent, this function exits immediately.
   * NOTE: In testing, it seems like calls meant for this function
   * "stack up" if the function doesn't return in time for the next
   * invocation.  Not sure if this will become an issue or not
   * NOTE2: Main engine loop is not transactional - instead, each
   * record processed is done in a separate transaction, to keep
   * things simple.  May need to rework this so it does it in
   * batches, but not just yet.  Trying to make it just simple send without
   * also being a giant pile of sloppy crap.
   */
  @Scheduled(fixedRate=TARGET_LOOP_TIME+LOOP_WAIT)
  public void engineLoop() {
   
    // don't re-enter
    synchronized(this) {
      if (engineLoopActive) { return; }
      engineLoopActive = true;
    }

   
      try {
     
      long myStartTime = System.currentTimeMillis();
      logger().debug("engineLoopIteration() - starting at {} in thread {}", myStartTime, Thread.currentThread().getName());
     
      // do a conditional refresh on the message stuff
      messageGroupSource.conditionalRefresh();
     
      boolean myShouldContinue = true;
     
      while (myShouldContinue) {
     
        // Process one inside it's own transaction -
        // We call via the Spring context so that we invoke
        // engineLoopProcessOne() via the AOP proxy, which is what
        // takes care of the transaction.  Just calling
        // this.engineLoopProcessOne() won't open any transaction.
        myShouldContinue = beanFactory.getBean("jrSendingEngine", SendingEngine.class)
                .engineLoopProcessOne();
       

        // check if time to bail out
        if (myStartTime + TARGET_LOOP_TIME < System.currentTimeMillis()) {
          return;
        }
       
      }
   
    }
     
      // just propagate the exception - should not be a normal occurence
    catch (Throwable t) {
      throw new RuntimeException(t);
    }
     
    finally {
      // make sure we always mark the loop as done
      engineLoopActive = false;
    }

  }
 
  /**
   * Used internally to process one record inside a transaction.
   * @return true if the loop calling this method should continue,
   * or false if we're done for now.
   */
  // must have it's own transaction
  @Transactional(propagation=Propagation.REQUIRES_NEW)
  public boolean engineLoopProcessOne() {
   
    // 1. select next batch size (maybe just 1 for now) where next
    //    consideration date is older than now
   
    Subscription mySubscription = mainDao.getNextSendableSubscription(SubscriptionStatus.ACTIVE);
   
    // if no result, then bail out, nothing to do
    if (mySubscription == null) {
      logger().debug("No more results, exiting loop");
      return false;
    }
   
    // the subscription we are now processing
    logger().debug("Got subscription with id: {}", mySubscription.getId());
   
    // look up the subscriber too
    Subscriber mySubscriber = mySubscription.getSubscriber();
   
    if (mySubscriber == null) {
      throw new RuntimeException("Data error, subscription with id " + mySubscription.getId() + " has null subscriber, should not be possible");
    }
   
    // Make sure we don't send to people who are asked off
   
    // if status is not okay...
    if (mySubscriber.getSubscriberStatus() != SubscriberStatus.OK) {
      // end this subscription
      endSubscription(mySubscription);
      // log for posterity
      logger().info("Was going to send to subscriber (email={}) but status is not OK, it's '{}', not sending and ending subscription.", mySubscriber.getEmail(), mySubscriber.getSubscriberStatus());
      // bail out
      return true;
    }
   
    // 2. for this Subscription, find last message processed in
    //    the list for that group
   
    // first make sure we have a corresponding message group
    MessageGroup myMessageGroup = messageGroupSource.getMessageGroupByName(mySubscription.getMessageGroupName());
   
    // if no such message group, then we just get rid of the
    // subscription (lose the date so it doesn't get pulled back up);
    // also catch the case where no subscriber record, should never
    // happen, but adding scenario for robustness)
    if (myMessageGroup == null || mySubscriber == null) {
     
      logger().warn(
          "Either message group or subscriber was null, " +
          "bailing out (messageGroup: {}, subscriber: {})",
          myMessageGroup, mySubscriber);
     
      /* ====================================================== */
      /* clear next send date of subscription                   */
      /* ====================================================== */
      endSubscription(mySubscription);
      return true;
    }
   
    // get the name of the last message
    String myLastMessageName = mySubscription.getLastMessageName();
    logger().debug("myLastMessageName: {}", myLastMessageName);
   
    // if last message exists
    int mySendMessageRefIndex = myMessageGroup.indexOfName(myLastMessageName);
    if (mySendMessageRefIndex >= 0) {
      // increment to next one to get the message we should send
      mySendMessageRefIndex++;
    }
    // if last message name was null, then start at zero
    if (myLastMessageName == null) {
      mySendMessageRefIndex = 0;
    }
   
    logger().debug("myLastMessageRefIndex: {}", mySendMessageRefIndex);
   
   
    // 3. get the next message
   
    List<MessageRef> myMessageRefList = myMessageGroup.getMessageRefList();
   
    // the MessageRef that we are to send
    MessageRef mySendMessageRef = null;
    if (mySendMessageRefIndex >= 0) {
      // make sure we haven't gone beyond the end of the messages
      if (myMessageRefList.size() > mySendMessageRefIndex) {
        mySendMessageRef = myMessageRefList.get(mySendMessageRefIndex);
      }
    }
    logger().debug("about to send mySendMessageRef: {}", mySendMessageRef);

   
    // no message to send (either couldn't find last message or
    // there is no next message); also check for the case where
    // the last send date is not set (should never happen, but just
    // to keep it from breaking the system we treat it like end
    // of list)
    if (mySendMessageRef == null || mySubscription.getLastSendDate() == null) {
      logger().debug("no more messages to send for this subscription");
      /* ====================================================== */
      /* clear next send date of subscription                   */
      /* ====================================================== */
      endSubscription(mySubscription);
      return true;
    }
   

    if (mySubscription.getLastSendDate() == null) {
      logger().warn("last_send_date was null on subscription, should never happen");
    }
   
    // 3a.read the time on it and recalculate, confirm that
    //    time is now past
   
    Long myWait = mySendMessageRef.getWaitAfterLastMessage();
    if (myWait == null) {
      /* ====================================================== */
      /* clear next send date of subscription                   */
      /* ====================================================== */
      logger().warn("'wait after last message' value was not set for this message, ending the subscription");
      endSubscription(mySubscription);
      return true;
    }
   
    // 3b.if not past then set next consideration date to recalced date
    //    and kick back
   
    // see if it's time to send yet
    long myLastSendDate = mySubscription.getLastSendDate().getTime();
    if (myLastSendDate + myWait > new Date().getTime()) {
      logger().debug("it is not yet time to send this message, putting it back for later");
      // not time to send (possibly message changed since
      // this subscription record was last edited - also first
      // subscription looks like this);
      // reset the nextSendDate and move on
      mySubscription.setNextSendDate(new Date(myLastSendDate + myWait));
      mainDao.persist(mySubscription);
      return true;
    }
   
   
    // 4. render and send message
   
    boolean mySendWorked = sendMessage(
            mySendMessageRef,
            myMessageGroup,
            mySubscriber,
            mySubscription,
            LogEntryType.MESSAGE_SENT
        );
   
    // if the sending was skipped...
    if (!mySendWorked) {
     
      // write a log entry for the skipping
      mainDao.logEntry
            (
                 LogEntryType.MESSAGE_SKIPPED,
                 mySubscriber,
                 myMessageGroup.getName(),
                 propUtil.mkprops(
                     "message_name", mySendMessageRef.getName()
                     )
            );

    }
   
   
   
   
    // 5. get next message in the list and recalculate date based on that,
    //    update last message name and save
   
    // update the last sent info
    mySubscription.setLastMessageName(mySendMessageRef.getName());
    mySubscription.setLastSendDate(new Date());
   
    // index of next message
    int myNextMessageRefIndex = mySendMessageRefIndex + 1;
   
   
    // 5a.if no more messages, then next consideration date is set to null
   
    if (myNextMessageRefIndex >= myMessageRefList.size()) {
      mySubscription.setNextSendDate(null);
    }
   
    // if we have a next message, then calc when it fires
    else {
     
      MessageRef myNextMessageRef = myMessageRefList.get(myNextMessageRefIndex);
      Long myNextWait = myNextMessageRef.getWaitAfterLastMessage();
      mySubscription.setNextSendDate(
            new Date(mySubscription.getLastSendDate().getTime() + myNextWait)
          );

    }
   
    // save the changes to the subscription
    mainDao.persist(mySubscription);
   
    if (mySubscription.getNextSendDate() == null) {
     
      // write a log entry for the completion of the subscription
      mainDao.logEntry
          (
               LogEntryType.SUBSCRIPTION_DONE,
               mySubscriber,
               myMessageGroup.getName(),
               propUtil.mkprops(
                   "message_name", mySendMessageRef.getName()
                   )
          );
    }

   
    // as far as we know, there should be more to process...
    return true;
   
  }
 
  /**
   * Ends a subscription by setting the nextSendDate to null
   *
   * @param aEntityManager
   * @param aSubscription
   */
  protected void endSubscription(Subscription aSubscription) {
    aSubscription.setNextSendDate(null);
    aSubscription.setSubscriptionStatus(SubscriptionStatus.INACTIVE);
    mainDao.persist(aSubscription);
  }
 
  /**
   * Delays a subscription by setting the nextSendDate to a day from now
   *
   * @param aEntityManager
   * @param aSubscription
   */
  protected void delaySubscription(Subscription aSubscription) {
    aSubscription.setNextSendDate(DateTime.now().plusMinutes(10).toDate());
    mainDao.persist(aSubscription);
  }
 
  /**
   * Perform an individual message send.
   *
   */
  @Transactional(propagation=Propagation.REQUIRED)
  public boolean sendMessage
            (
                MessageRef aSendMessageRef,
                MessageGroup aMessageGroup,
                Subscriber aSubscriber,
                Subscription aSubscription,
                LogEntryType aLogEntryType
            ) {
   
    logger().debug("Rendering message...");
    long t1 = System.currentTimeMillis();
    MimeMessage myMimeMessage = javaMailSender.createMimeMessage();
   
    // look up the send config
    SendConfig mySendConfig = sendConfigGroup.lookupSendConfig
        (
            aSubscriber,
            aSubscription,
            aMessageGroup,
            aSendMessageRef
        );
   
    if (mySendConfig == null) {
      throw new IllegalStateException("No SendConfig, cannot send mail like this!  Please define at least a default...");
    }
   
    // populate message
    if (aSendMessageRef.populateMessage(myMimeMessage, mySendConfig, aSubscriber, aSubscription)) {
   
      // if population worked, then send
      logger().debug("Message rendered, that took {} milliseconds", System.currentTimeMillis() - t1);
     
      // do the message sending
      // note - if this errors, fine, let it error.  it should spew
      // out in the logs and the admin can fix it - we haven't marked
      // message as sent yet...
      logger().debug("Transmitting message (using JavaMailSender)...");
      long t2 = System.currentTimeMillis();
      javaMailSender.send(myMimeMessage);
      logger().debug("Message transmitted, that took {} milliseconds", System.currentTimeMillis() - t2);

     
      // write a log entry for the sending
      mainDao.logEntry
            (
                 aLogEntryType,
                 aSubscriber,
                 aMessageGroup.getName(),
                 propUtil.mkprops(
                     "message_name", aSendMessageRef.getName()
                     )
            );
     
      return true;
     
    }
   
    // message told us to skip
    else {
     
      logger().debug("Message rendered (took {} milliseconds), but result is to skip sending the message (html and text body both empty); skipping", System.currentTimeMillis() - t1);
     
      return false;
    }

   
  }
 
}
TOP

Related Classes of org.jresponder.engine.SendingEngineImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.