Cache.java

package sk.iway.iwcm;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import sk.iway.iwcm.doc.DocDetails;
import sk.iway.iwcm.system.cluster.ClusterDB;
import sk.iway.iwcm.users.UserDetails;


/**
 *  Cache.java - cache pre casto pouzivane objekty, ktorych generovanie je pomale
 *
 *@Title        WebJET
 *@Company      Interway s.r.o. (www.interway.sk)
 *@Copyright    Interway s.r.o. (c) 2001-2004
 *@author       $Author: jeeff $
 *@version      $Revision: 1.16 $
 *@created      $Date: 2009/06/02 06:15:56 $
 *@modified     $Date: 2009/06/02 06:15:56 $
 */
public class Cache
{
	/**
	 * A list of objects which will be notified about any change in the cached objects
	 */
	private static final List<CacheListener> listeners = new Vector<>();

	/**
	 * Subscribes to the new events in Cache
	 *
	 * @param theListener
	 */
	public static void subscribe(CacheListener theListener)
	{
		listeners.add(theListener);
	}

	/**
	 * No longer willing to hear about new events in Cache
	 * @param theListener
	 */
	public static void unsubscribe(CacheListener theListener)
	{
		listeners.remove(theListener);
	}

	/**
	 * Nazov, pod ktorym sa tento objekt nachadza v ServletContexte
	 */
	public static final String CONTEXT_NAME = "sk.iway.iwcm.Cache";

	private Hashtable<String, CacheBean> objectCache; //NOSONAR

	//timestamp poslednej kontroly nepotrebnych objektov
	private long lastRemoveTime = 0;
	//pocet ms v ktorych sa bude vykonavat kontrola nepotrebnych objektov
	private long REMOVE_CHECK = 300000; //NOSONAR

	//pocet ms pre ktore sa pouzije smart refresh
	private long SMART_REFRESH_TIME = 30000; //NOSONAR

	/**
	 * Vrati instanciu Cache
	 * @return
	 */
	public static Cache getInstance()
	{
		return(getInstance(false));
	}

	/**
	 * Vrati instanciu chache
	 * @param forceRefresh - ak je true, vytvori sa nanovo (zabudne stare objekty)
	 * @return
	 */
	public static synchronized Cache getInstance(boolean forceRefresh)
	{
		//try to get it from server space
		if (forceRefresh == false)
		{
			Object o = Constants.getServletContext().getAttribute(CONTEXT_NAME);
			if (o instanceof Cache)
			{
				Cache cache = (Cache)o;
				return (cache);
			}
		}
		return (new Cache());
	}

	/**
	 * Privatny konstruktor, Cache musi byt ziskana cez getInstance()
	 */
	private Cache()
	{
		Logger.println(this,"Cache: constructor ["+Constants.getInstallName()+"]");
		objectCache = new Hashtable<>();

		REMOVE_CHECK = Constants.getInt("cacheRemoveCheckSeconds") * 1000L;
		SMART_REFRESH_TIME = Constants.getInt("cacheSmartRefreshSeconds") * 1000L;

		Constants.getServletContext().setAttribute(CONTEXT_NAME, this);
	}

	private void removeCheck()
	{
		long currentTime = System.currentTimeMillis();
		if (lastRemoveTime+REMOVE_CHECK > currentTime) return;

		lastRemoveTime = System.currentTimeMillis();

		List<String> removeNames = new ArrayList<>();
		try
		{
			Enumeration<CacheBean> e = objectCache.elements();
			while (e.hasMoreElements())
			{
				CacheBean cb = e.nextElement();
				if (cb.getExpiryTime() < currentTime)
				{
					removeNames.add(cb.getName());
				}
			}
		}
		catch (Exception ex)
		{

		}

		Iterator<String> iter = removeNames.iterator();
		while (iter.hasNext())
		{
			removeObject(iter.next());
		}

	}

	/**
	 * Vymaze celu cache
	 *
	 */
	public void clearAll()
	{
		try
		{
			Enumeration<String> e = objectCache.keys();
			while (e.hasMoreElements())
			{
				removeObject(e.nextElement());
			}
		}
		catch (Exception ex)
		{
			sk.iway.iwcm.Logger.error(ex);
		}

		objectCache = new Hashtable<>();
	}

	/**
	 * Vrati pocet poloziek v cache
	 * @return
	 */
	public int getSize()
	{
		return(objectCache.size());
	}

	/**
	 * Ziska objekt z cache, vrati null, ak sa v cache nenachadza (alebo cas exspiroval)
	 * @param name - symbolicke meno objektu v cache
	 * @return
	 */
	public Object getObject(String name)
	{
		removeCheck();

		//Logger.println(this,"Cache.getObject("+name+")");

		Object object = objectCache.get(name);
		Object returnObject = null;
		if (object != null)
		{
			CacheBean cb = (CacheBean)object;
			long currentTime = Tools.getNow();
			//Logger.debug(this, "Cache.getObject("+name+") - object found now="  + currentTime + " exp=" + cb.getExpiryTime() + " diff="+(cb.getExpiryTime() - currentTime));
			if (cb.getExpiryTime() > currentTime)
			{
				if (cb.isAllowSmartRefresh() && cb.isSmartRefreshed()==false &&
					 (cb.getExpiryTime()-(SMART_REFRESH_TIME)) < System.currentTimeMillis())
				{
					//30 sekund pred vyprsanim vratime null
					cb.setSmartRefreshed(true);

					//Logger.debug(Cache.class, "SmartRefresh: "+cb.getName());

					return null;
				}
				//Logger.debug(this, "mam v cache: " + name);
				returnObject = cb.getObject();
			}
			else
			{
				//Logger.debug(this, "Removing expired object "+name);
				//vyhod objekt z cache
				removeObject(name);
			}
		}

		return(returnObject);
	}

	/**
	 * vrati objekt uz pretypovany (nemam rad ked musim po vrateni objekt pretypovavat :) )
	 * @param name
	 * @param type
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <R extends Object> R getObject(String name, Class<R> type)
	{
		Object result = getObject(name);
		if (result!=null && type.isAssignableFrom(result.getClass()))
		{
			return (R) result;
		}
		return null;
	}

	/**
	 * Vrati zoznam vsetkych klucov z cache
	 * @return
	 */
	public Enumeration<String> getAllKeys()
	{
		return objectCache.keys();
	}

	/**
	 * Vrati zoznam vsetkych objektov v cache
	 * @return
	 */
	public Enumeration<CacheBean> getAllElements()
	{
		return objectCache.elements();
	}

	/**
	 * Vlozi objekt do cache
	 * @param name - symbolicke meno objektu v cache
	 * @param object - objekt, ktory sa ma vlozit
	 * @param cacheInMinutes - pocet minut, pocas ktorych sa objekt bude v cache nachadzat
	 */
	public void setObject(String name, Object object, int cacheInMinutes)
	{
		setObjectSeconds(name, object, cacheInMinutes*60);
	}

	/**
	 * Vlozi objekt do cache, pouzije SMART Refresh
	 * @param name - symbolicke meno objektu v cache
	 * @param object - objekt, ktory sa ma vlozit
	 * @param cacheInSeconds - pocet sekund, pocas ktorych sa objekt uchova
	 */
	public void setObjectSeconds(String name, Object object, int cacheInSeconds)
	{
		setObjectSeconds(name, object, cacheInSeconds, true);
	}

	/**
	 * Vlozi objekt do cache
	 * @param name - symbolicke meno objektu v cache
	 * @param object - objekt, ktory sa ma vlozit
	 * @param cacheInSeconds - pocet sekund, pocas ktorych sa objekt uchova
	 * @param allowSmartRefresh - ak je nastavene na true, tak tesne pred vyprsanim objektu
	 * 			zo session je vratene null, aby dany thread mohol objekt znova naplnit (SmartCahce)
	 */
	public void setObjectSeconds(String name, Object object, int cacheInSeconds, boolean allowSmartRefresh)
	{
		long expiryTime = Tools.getNow() + (1000l * cacheInSeconds);
		setObjectByExpiry(name, object, expiryTime, allowSmartRefresh);
	}

	/**
	 * Vlozi objekt do cache s nastavenym casom exspiracie
	 * @param name
	 * @param object
	 * @param expiryTime
	 * @param allowSmartRefresh
	 */
	public void setObjectByExpiry(String name, Object object, long expiryTime, boolean allowSmartRefresh)
	{
		CacheBean cb = new CacheBean();
		cb.setName(name);
		cb.setObject(object);
		cb.setExpiryTime(expiryTime);
		cb.setAllowSmartRefresh(allowSmartRefresh);

		//Logger.debug(this,"Cache.setObject("+name+") " + Tools.formatDateTimeSeconds(Tools.getNow()) + " exp=" + Tools.formatDateTimeSeconds(expiryTime));

		//vloz ho do HashTabulky
		removeObject(name);

		objectCache.put(name, cb);

		synchronized (listeners)
		{
			for (CacheListener theListener : listeners)
			{
				try{
					theListener.objectAdded(cb);
				}catch (Exception e) {sk.iway.iwcm.Logger.error(e);}
			}
		}
	}

	/**
	 * Nastavi uz existujucemu objektu v cache novy cas exspiracie
	 * @param name
	 * @param expiryTime
	 */
	public void setObjectExpiryTime(String name, long expiryTime) {
		CacheBean cb = objectCache.get(name);
		if (cb != null) {
			cb.setExpiryTime(expiryTime);
		}
	}

	public void removeObject(String name)
	{
		removeObject(name, false);
	}

	/**
	 * Odstrani zadany objekt z cahce. Ak je refreshCluster true odstani sa aj z ostatnych nodov clustra
	 * @param name
	 * @param refreshCluster
	 */
	public void removeObject(String name, boolean refreshCluster)
	{
		//najskor refresh, lebo na tomto node nemusi existovat
		if (refreshCluster)
		{
			ClusterDB.addRefresh(DB.prepareString("sk.iway.iwcm.Cache-" + name, 250));
			Adminlog.add(Adminlog.TYPE_DATA_DELETING, "Deleting cache, key= " + name, -1, -1);
		}

		Object o = objectCache.get(name);
		if (o == null)
			return;

		CacheBean theBean = (CacheBean)o;

		synchronized (listeners)
		{
			for (CacheListener theListener : listeners)
			{
				try{
					theListener.objectRemoved(theBean);
				}catch (Exception e) {sk.iway.iwcm.Logger.error(e);}
			}
		}

		objectCache.remove(name);
	}

	/**
	 * Vymaze z cache objekty zacinajuce na dane meno
	 * @param name
	 */
	public void removeObjectStartsWithName(String name)
	{
		removeObjectStartsWithName(name, false);
	}

	/**
	 * Vymaze z cache objekty zacinajuce na dane meno. Ak je refreshCluster true odstani sa aj z ostatnych nodov clustra
	 * @param name
	 * @param refreshCluster
	 */
	public void removeObjectStartsWithName(String name, boolean refreshCluster)
	{
		List<String> removeNames = new ArrayList<>();
		try
		{
			Enumeration<CacheBean> e = objectCache.elements();
			while (e.hasMoreElements())
			{
				CacheBean cb = e.nextElement();
				if (cb.getName().startsWith(name))
				{
					removeNames.add(cb.getName());
				}
			}
		}
		catch (Exception ex)
		{

		}

		if (refreshCluster) {
			ClusterDB.addRefresh(DB.prepareString("sk.iway.iwcm.Cache:startsWithName-" + name, 250));
			Adminlog.add(Adminlog.TYPE_DATA_DELETING, "Deleting cache:startsWithName, key= " + name, -1, -1);
		}

		removeObject(name, false);

		Iterator<String> iter = removeNames.iterator();
		while (iter.hasNext())
		{
			removeObject(iter.next(), false);
		}
	}

	/**
	 * Stiahne url a ulozi ho do cache na cacheInMinutes minut. Podporuje iba GET
	 * @param url - url adresa stranky (sluzi aj ako kluc do cache)
	 * @param cacheInMinutes - pocet minut, pocas ktorych sa bude drzat v cache
	 * @return
	 */
	public String downloadUrl(String url, int cacheInMinutes)
	{
		//meno v cache
		String name = "downloadUrl."+url;

		Object o = getObject(name);
		if (cacheInMinutes>0 && o!=null && o instanceof String)
		{
			//parada, mame to v cache
			String data = (String)o;
			return(data);
		}

		//stiahni to a uloz do cache
		String data = Tools.downloadUrl(url);
		if (cacheInMinutes>0 && data != null && data.length()>0)
		{
			setObject(name, data, cacheInMinutes);
		}

		return(data);
	}

	public void onDocChange(DocDetails doc)
	{
		String cacheOnDocCahngeMode = Constants.getString("cacheOnDocCahngeMode");
		if (Tools.isEmpty(cacheOnDocCahngeMode) || cacheOnDocCahngeMode.length()<2 || "none".equalsIgnoreCase(cacheOnDocCahngeMode)) return;

		if ("all".equalsIgnoreCase(cacheOnDocCahngeMode))
		{
			try
			{
				Logger.debug(Cache.class, "onDocChange, removing ALL ");
				clearAll();
			}
			catch (Exception e)
			{
				sk.iway.iwcm.Logger.error(e);
				Cache.getInstance(true);
			}
		}
		else if ("include".equalsIgnoreCase(cacheOnDocCahngeMode) || "groupid".equalsIgnoreCase(cacheOnDocCahngeMode))
		{
			try
			{
				List<String> objNamesToRemove = new ArrayList<>();

				//priprav zoznam
				Enumeration<String> e = objectCache.keys();
				while (e.hasMoreElements())
				{
					String objName = e.nextElement();

					if ("groupid".equalsIgnoreCase(cacheOnDocCahngeMode))
					{
						objName = objName.toLowerCase();
						if (objName.indexOf("groupid="+doc.getGroupId()+",")!=-1 ||
							 objName.indexOf("groupid="+doc.getGroupId()+" ")!=-1 ||
							 objName.indexOf("groupid='"+doc.getGroupId()+"'")!=-1 ||
							 objName.indexOf("groupid=\""+doc.getGroupId()+"\"")!=-1
						)
						{
							objNamesToRemove.add(objName);
						}
					}
					else
					{
						if (objName.indexOf("!INCLUDE")!=-1 || objName.indexOf("writeTag_")!=-1) objNamesToRemove.add(objName);
					}
				}

				//vymaz data
				for (String objName : objNamesToRemove)
				{
					CacheBean c = objectCache.get(objName);
					if (c == null) continue;

					if (c.isAllowSmartRefresh())
					{
						Logger.debug(Cache.class, "onDocChange, smart removing "+objName);
						//nastav cas pre refreshnutie na aktualny+SMART refresh-sekunda aby sa najblizsie objekt refreshol
						c.setExpiryTime(System.currentTimeMillis()+SMART_REFRESH_TIME-1000);
					}
					else
					{
						Logger.debug(Cache.class, "onDocChange, removing "+objName);
						removeObject(objName);
					}
				}
			}
			catch (Exception e)
			{
				sk.iway.iwcm.Logger.error(e);
				Cache.getInstance(true);
			}
		}
	}

	/**
	 * Ziska timestamp exspiracie objektu z cache, vrati null, ak sa objekt v cache nenachadza (alebo cas exspiroval)
	 * @param name - symbolicke meno objektu v cache
	 * @return
	 */
	public Long getObjectExpiryTime(String name)
	{
		removeCheck();

		//Logger.println(this,"Cache.getObject("+name+")");

		Object object = objectCache.get(name);
		Long returnObject = null;
		if (object != null)
		{
			CacheBean cb = (CacheBean)object;
			//Logger.println(this,"Cache.setObject("+name+") - object found "  + Tools.getNow() + " exp=" + cb.getExpiryTime());
			if (cb.getExpiryTime() > System.currentTimeMillis())
			{
				//Logger.println(this,"mam v cache: " + name);
				returnObject = cb.getExpiryTime();
			}
			else
			{
				//vyhod objekt z cache
				removeObject(name);
			}
		}

		return(returnObject);
	}

	/**
	 * Vrati prefix klucov pre zadaneho usera
	 * @param user
	 * @return
	 */
	private String getUserPrefix(UserDetails user)
	{
		String loginName = "notLoggedUser";
		if (user != null) loginName = user.getLogin();

		return "usr."+loginName+".";
	}

	/**
	 * Vlozi objekt do cache pre daneho pouzivatela
	 * @param user - prihlaseny pouzivatel
	 * @param name - symbolicke meno objektu v cache
	 * @param object - objekt, ktory sa ma vlozit
	 * @param cacheInMinutes - pocet minut, pocas ktorych sa objekt bude v cache nachadzat
	 */
	public void setUserObject(UserDetails user, String name, Object object, int cacheInMinutes)
	{
		setObjectSeconds(getUserPrefix(user) + name, object, cacheInMinutes*60);
	}

	/**
	 * vrati objekt uz pretypovany (nemam rad ked musim po vrateni objekt pretypovavat :) )
	 * @param user - prihlaseny pouzivatel
	 * @param name
	 * @param type
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <R extends Object> R getUserObject(UserDetails user, String name, Class<R> type)
	{
		Object result = getUserObject(user, name);
		if (result!=null && type.isAssignableFrom(result.getClass()))
		{
			return (R) result;
		}
		return null;
	}

	/**
	 * Ziska objekt z cache, vrati null, ak sa v cache nenachadza (alebo cas exspiroval)
	 * @param user - prihlaseny pouzivatel
	 * @param name - symbolicke meno objektu v cache
	 * @return
	 */
	public Object getUserObject(UserDetails user, String name)
	{
		return getObject(getUserPrefix(user)+name);
	}

	/**
	 * Odstrani z cache vsetky objekty zadaneho pouzivatela
	 * @param user
	 */
	public void removeUserAllUserObjects(UserDetails user)
	{
		if (user != null)
		{
			removeObjectStartsWithName(getUserPrefix(user), true);
		}
	}
}