DomainThrottle.java

package sk.iway.iwcm.dmail;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.eclipse.persistence.jpa.JpaEntityManager;

import sk.iway.iwcm.DB;
import sk.iway.iwcm.DBPool;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.system.jpa.JpaTools;


/**
 *  DomainThrottle.java
 *
 *@Title        webjet7
 *@Company      Interway s.r.o. (www.interway.sk)
 *@Copyright    Interway s.r.o. (c) 2001-2013
 *@author       $Author: jeeff mhalas $
 *@version      $Revision: 1.3 $
 *@created      Date: 23.7.2013 16:01:40
 *@modified     $Date: 2004/08/16 06:26:11 $
 */
public class DomainThrottle
{

	private SortedMap<Long, String> map ;
	private Map<String,Long> lastEmails;
	private static DomainThrottle instance = null;
	private Map<String,DomainLimitBean> domainLimits;
	private long maxTimeLimit = 0;

	private DomainThrottle(){
		super();
		init();
	}

	private void init(){
		domainLimits = Collections.emptyMap();
		maxTimeLimit = 0;
		Collection<DomainLimitBean> allLimits = DomainLimitsDB.getInstance(true).getAll();
		try {
			if(allLimits != null) {
				//domainLimits = Lambda.index(allLimits, Lambda.on(DomainLimitBean.class).getDomain());
				allLimits.forEach(domainLimit -> domainLimits.put(domainLimit.getDomain(), domainLimit));
			}
			if(!domainLimits.isEmpty()) {
				//maxTimeLimit = Lambda.max(domainLimits.values(), Lambda.on(DomainLimitBean.class).getTimeUnit().toMillis(1));
				for (DomainLimitBean limit : domainLimits.values()) {
					if (maxTimeLimit < limit.getMinDelay()) maxTimeLimit = limit.getMinDelay();
				}
			}
		} catch (Exception ex) {
			Logger.error(DomainThrottle.class, ex);
		}
		map = new TreeMap<>();
		lastEmails = new HashMap<>();
		loadMap();
	}

	/**
	 * Zinicializuje mapu Cas->domena z tabulky emailov, berie len odoslane a cas len od nahdlhsieho nastaveneho limitu
	 */
	private void loadMap()
	{
		String exceptionMessage = null;
		Connection db_conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		String sql = "SELECT recipient_email, sent_date FROM emails WHERE sent_date IS NOT NULL AND sent_date >= ? ";
		try
		{
			db_conn = DBPool.getConnection();
			ps = db_conn.prepareStatement(sql);
			int parameterIndex = 1;
			ps.setTimestamp(parameterIndex++, DB.getDbTimestamp(System.currentTimeMillis()-maxTimeLimit));
			rs = ps.executeQuery();
			String domain = "";
			while (rs.next())
			{
				domain = getDomainFromEmail(rs.getString("recipient_email"));
				if(Tools.isNotEmpty(domain))
				{
					map.put(DB.getDbTimestamp(rs, "sent_date"), domain);
					if(domainLimits.containsKey(domain))
						lastEmails.put(domain, DB.getDbTimestamp(rs, "sent_date"));
				}
			}
			rs.close();
			ps.close();
			db_conn.close();
			rs = null;
			ps = null;
			db_conn = null;
		}
		catch (Exception ex)
		{
			exceptionMessage = ex.getMessage();
			IllegalStateException exception = new IllegalStateException(exceptionMessage);
			exception.initCause(ex);
			throw exception;
		}
		finally
		{
			try
			{
				if (rs != null)
					rs.close();
				if (ps != null)
					ps.close();
				if (db_conn != null)
					db_conn.close();
			}
			catch (Exception ex2)
			{
				sk.iway.iwcm.Logger.error(ex2);
			}
		}

	}

	/**
	 * dummy impl of getting domain from email address
	 * @param string
	 * @return
	 */
	public static String getDomainFromEmail(String address)
	{
		String domain = "";
		if(address.indexOf("@")!=-1)
		{
			domain = address.substring(address.indexOf("@")+1);
		}
		return domain;
	}

	public static DomainThrottle getInstance(){
		DomainThrottle throttle = instance;
		if(throttle == null)
		{
			synchronized(DomainThrottle.class)
			{
				throttle = instance;
				if(throttle == null)
					instance = throttle = new DomainThrottle();
			}
		}
		return throttle;
	}

	/**
	 * Zisti ci je mozne na zaklade nastaveny limitov poslat email na danu domenu
	 * najpr sa kontroluje ci ma domena nejaky limit, ak nie poslanie sa hned povoli
	 * ak ano, najprv sa skontroluje ci posledny email na danu domenu neodisiel uz davnejsie ako je
	 * minimalne delay pre danu domenu, ak ano tak sa poslanie povoli, ak nie
	 * tak sa spocita pocet emailov na danu domenu za cas od teraz po casovy limit a ak nie je limit prekroceny
	 * tak sa odoslanie povoli.
	 * @param domain domena
	 * @return
	 */
	public synchronized boolean canSend(String domain)
	{
		boolean canSend = true;

		if(domainLimits.get(domain)==null && domainLimits.get("*")!=null)
		{
			cloneGenericLimit(domain);
		}
		DomainLimitBean domainLimit=domainLimits.get(domain);
		if(domainLimit!=null)
		{
			long now = System.currentTimeMillis();
			//minimalny delay
			if(domainLimit.isDelayActive())
			{
				if(lastEmails.containsKey(domain))
				{
					long lastEmailTime = lastEmails.get(domain);
					if(lastEmailTime <= (now-domainLimit.getMinDelay())) canSend = true;
					else {
						canSend = false;
						Logger.debug(getClass(), "Domain: "+domain +" -> Min delay not reached yet");
					}
				}
			}
			//este skontrolujeme limit poctu mailov za casovu jednotku
			if(domainLimit.isActive() && canSend)
			{
				//Logger.debug(getClass(), "getting count of emails sent to "+ domain + " from: "+(now-domainLimit.getTimeUnit().toMillis(1)) + " to: "+now);
				int sentEmailsCount = countEmailsSentToDomain(now-domainLimit.getTimeUnit().toMillis(1), now, domain);
				if(sentEmailsCount < domainLimit.getLimit()) canSend = true;
				else{
					canSend = false;
					//Logger.debug(getClass(), "Domain: "+domain +" -> Limit over time crossed");
				}
			}
		}
		Logger.debug(getClass(), "Domain: "+domain +" Can send: "+canSend);
		return canSend;
	}

	/**
	 * Spravi kopiu generickeho limitu pre domenu ktora nema explicitne definovane limity
	 * tuto kopiu ale neulozi do DB, prepocita maxTimeLimit a oshapuje mapu emailov ak treba
	 * @param domain domena
	 */
	protected void cloneGenericLimit(String domain)
	{
		Logger.debug(getClass(), "No limit defined for domain " + domain + " , generic limit enabled, copying...");
		DomainLimitBean matrix = domainLimits.get("*");
		JpaEntityManager manager = JpaTools.getEclipseLinkEntityManager();
		manager.detach(matrix);
		matrix.setDomain(domain);
		matrix.setId(0);
		domainLimits.put(domain, matrix);
		Logger.debug(getClass(), "Limit for domain: " + domain + " added. Limit: " +matrix);
		Logger.debug(getClass(), "Recalculating maxTimeLimit...");
		//maxTimeLimit = Lambda.max(domainLimits.values(), Lambda.on(DomainLimitBean.class).getTimeUnit().toMillis(1));
		maxTimeLimit = 0;
		for (DomainLimitBean limit : domainLimits.values()) {
			if (maxTimeLimit < limit.getMinDelay()) maxTimeLimit = limit.getMinDelay();
		}
		Logger.debug(getClass(), "maxTimeLimit: " + maxTimeLimit);
		long currentTime = System.currentTimeMillis();
		if(!map.isEmpty())
		{
			long from = currentTime - maxTimeLimit;
			long to  = currentTime;
			if(from < this.map.firstKey()) from = this.map.firstKey();
			if(to > this.map.lastKey()) to = this.map.lastKey();
			if(from <= to ) map = new TreeMap<>(map.subMap(from, to));
		}
	}

	/**
	 * toto by malo zistit kolko mailov bolo na danu domenu poslanych v danom rozsahu casov
	 * snad to bude dostatocne rychle
	 * otazka concurent modification?
	 * @param from
	 * @param to
	 * @param domain
	 * @return
	 */
	private int countEmailsSentToDomain(long from, long to, String domain)
	{
		if(this.map.isEmpty()) return 0;
		if(from < this.map.firstKey()) from = this.map.firstKey();
		if(to > this.map.lastKey()) to = this.map.lastKey();
		if(from > to){
			Logger.debug(getClass(), "from key > to, returning 0");
			return 0;
		}
		SortedMap<Long,String> subMap = this.map.subMap(from, to);
		int count = 0;
		for(String d : subMap.values())
		{
			if(d.equals(domain)) count++;
		}
		return count;
	}

	/**
	 * This method will reload domain limits settings
	 * recalculate longest limit and shape domain records
	 * according to longest limit (keep records only from longest limit to present)
	 */
	public synchronized void refresh()
	{
		Collection<DomainLimitBean> allLimits = DomainLimitsDB.getInstance(true).getAll();
		try {
			if(allLimits != null) {
				//domainLimits = Lambda.index(allLimits, Lambda.on(DomainLimitBean.class).getDomain());
				domainLimits = Collections.emptyMap();
				allLimits.forEach(limit -> domainLimits.put(limit.getDomain(), limit));
			}
			if(!domainLimits.isEmpty()) {
				//maxTimeLimit = Lambda.max(domainLimits.values(), Lambda.on(DomainLimitBean.class).getTimeUnit().toMillis(1));
				maxTimeLimit = 0;
				for (DomainLimitBean limit : domainLimits.values()) {
					if (maxTimeLimit < limit.getMinDelay()) maxTimeLimit = limit.getMinDelay();
				}
			}
			long currentTime = System.currentTimeMillis();
			if(!map.isEmpty())
			{
				long from = currentTime - maxTimeLimit;
				long to  = currentTime;
				if(from < this.map.firstKey()) from = this.map.firstKey();
				if(to > this.map.lastKey()) to = this.map.lastKey();
				map = new TreeMap<>(map.subMap(from, to));
			}
		} catch (Exception e) {
			Logger.error(DomainThrottle.class, e);

			map = new TreeMap<>();
			lastEmails = new HashMap<>();
			loadMap();
		}

	}

	/**
	 * Prida odoslany email do tabuliek
	 * @param domain domena
	 * @param timeSent cas odoslania v milisekundach
	 */
	public synchronized void  addEmail(String domain, long timeSent)
	{
		lastEmails.put(domain, timeSent);
		map.put(timeSent, domain);
	}
}