/**
 * HibernatePersistenceService.java
 * Created Aug 1, 2007
 * Copyright (c) Tandberg Television 2007
 */
package com.tandbergtv.workflow.driver.internal;

import java.io.Serializable;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import com.tandbergtv.workflow.core.CustomToken;
import com.tandbergtv.workflow.core.WorkflowProcess;
import com.tandbergtv.workflow.core.service.ServiceRegistry;
import com.tandbergtv.workflow.core.service.cache.ICacheService;
import com.tandbergtv.workflow.driver.DriverRuntimeException;
import com.tandbergtv.workflow.driver.service.IPersistenceService;

/**
 * Default persistence service implementation which uses Hibernate as the ORM.
 * 
 * Reads are looked up from the cache first. However, writes do not explicitly go through the cache;
 * the assumption is that if the process is in the cache, that object instance will be the only one. 
 * 
 * @author Sahil Verma
 */
public class HibernatePersistenceService implements IPersistenceService {

	private ICacheService<WorkflowProcess> cache;
	
	private SessionFactory factory;
	
	private static final Logger logger = Logger.getLogger(HibernatePersistenceService.class);
	
	private static final String SERVICE_NAME = "Process Persistence";
	
	/**
	 * Creates a {@link HibernatePersistenceService}
	 */
	@SuppressWarnings("unchecked")
	public HibernatePersistenceService(SessionFactory factory) {
		this.factory = factory;
		this.cache = (ICacheService<WorkflowProcess>)ServiceRegistry.getDefault().lookup("Process Cache");
	}

	/**
	 * @return the cache
	 */
	public ICacheService<WorkflowProcess> getCache() {
		return this.cache;
	}

	/**
	 * @param cache the cache to set
	 */
	public void setCache(ICacheService<WorkflowProcess> cache) {
		this.cache = cache;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.IPersistenceService#get(java.io.Serializable)
	 */
	public WorkflowProcess get(Serializable id) {
		/* Look in the cache first */
		WorkflowProcess process = cache.get(id);
		
		if (process != null)
			return process;
		
		Session session = getSession();
		
		try {
			process = (WorkflowProcess)session.load(WorkflowProcess.class, id);
		} finally {
			closeSession(session);
		}
		
		return process;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.IPersistenceService#getToken(java.io.Serializable)
	 */
	public CustomToken getToken(Serializable id) {
		CustomToken token = null;
		
		/* Micro-optimization. Look in the cache of processes before going to the database */
		for (Serializable key : cache.getKeys()) {
			WorkflowProcess process = cache.get(key);

			/* 
			 * HACK We don't lock the cache, therefore someone could potentially have removed
			 * this element while we're iterating through the elements.
			 */
			if (process == null)
				continue;
			
			for (CustomToken t : process.findAllTokens()) {
				if (t.getId() == (Long)id)
					return t;
			}
		}
		
		/* Not in cache, hit the database */
		Session session = getSession();
		
		try {
			token = (CustomToken)session.load(CustomToken.class, id);
		} finally {
			closeSession(session);
		}

		return token;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.IPersistenceService#assignId(com.tandbergtv.workflow.core.WorkflowProcess)
	 */
	public Serializable create(WorkflowProcess process) {
		save(process);
		
		return process.getId();
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.IPersistenceService#save(com.tandbergtv.workflow.core.WorkflowProcess)
	 */
	public void save(WorkflowProcess process) {
		Session session = getSession();
		Transaction t = null;
		
		/*
		 * Only one thread is allowed to persist this object at any given time. This is a hack to
		 * work around StaleObjectExceptions when more than one thread has a session open containing
		 * the same process. Perhaps we should use LockModes.
		 */
		process.lock();
		
		try {
			t = session.beginTransaction();
			session.saveOrUpdate(process);
			t.commit();
		} catch (Exception e) {
			if (t != null)
				t.rollback();
			logger.error("Failed to save the process " + process, e);
			throw new DriverRuntimeException("Failed to save the process", e);
		} finally {
			closeSession(session);
			process.unlock();
		}
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.IPersistenceService#save(com.tandbergtv.workflow.core.CustomToken)
	 */
	public void save(CustomToken token) {
		save(token.getProcessInstance());
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.Service#getServiceName()
	 */
	public String getServiceName() {
		return SERVICE_NAME;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#start()
	 */
	public void start() {
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#stop()
	 */
	public void stop() {
	}

	private Session getSession() {
		return this.factory.openSession();
	}
	
	private void closeSession(Session session) {
		if (session != null)
			session.close();
	}	
}
