/*
 * Copyright (C) 2005 - 2011 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com.
 *
 * Unless you have purchased  a commercial license agreement from Jaspersoft,
 * the following license terms  apply:
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License  as
 * published by the Free Software Foundation, either version 3 of  the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero  General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public  License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.jaspersoft.jasperserver.war.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import javax.servlet.ServletContext;
import javax.sql.DataSource;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.util.JRLoader;
import net.sf.jasperreports.engine.util.JRSaver;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.context.ServletContextAware;

import com.jaspersoft.jasperserver.api.engine.common.service.EngineService;
import com.jaspersoft.jasperserver.api.metadata.common.service.RepositoryService;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.JdbcReportDataSource;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.JndiJdbcReportDataSource;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.ReportDataSource;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.service.ReportDataSourceService;
import com.jaspersoft.jasperserver.api.metadata.user.service.TenantService;
import com.jaspersoft.jasperserver.api.metadata.view.domain.FilterCriteria;

/**
 * @author Teodor Danciu (teodord@users.sourceforge.net)
 * @version $Id: HibernateLoggingService.java 8408 2007-05-29 23:29:12Z melih $
 */
public class HeartbeatBean implements ServletContextAware, HeartbeatContributor
{
	private static final Log log = LogFactory.getLog(HeartbeatBean.class);
	
	private static final String PROPERTY_VERSION = "version";
	private static final String PROPERTY_HEARTBEAT_ID = "heartbeat.id";
	private static final String PROPERTY_PERMISSION_GRANTED = "permission.granted";

	private static final HeartbeatContributor OPTOUT_HEARTBEAT_CONTRIBUTOR = 
		new HeartbeatContributor()
		{
			public void contributeToHttpCall(PostMethod post) 
			{
				post.addParameter("callCount", "-1");
			}
		};
	
	private ServletContext servletContext = null;
	private DataSource dataSource = null;
	private TenantService tenantService = null;
	private RepositoryService repositoryService = null;
	private EngineService engineService = null;
	private LocalesList localesList = null;
	private HeartbeatContributor optionalContributor = null;

	private boolean enabled = false;
	private boolean askForPermission = false;
	private boolean permissionGranted = false;
	private String url = null;
	private int maxCacheSize = 0;
	private long cacheSaveInterval = 0;
	
	private String localId = null;
	private String heartbeatId = null;
	private int callCount = 0;
	private Properties localIdProperties = new Properties();
	private long lastCacheSaveTime = 0;

	private String osName = null;
	private String osVersion = null;
	private String javaVendor = null;
	private String javaVersion = null;
	private String serverInfo = null;
	private String productName = null;
	private String productVersion = null;
	private String location = null;
	private String dbName = null;
	private String dbVersion = null;

	private HeartbeatInfoCache clientInfoCache = new HeartbeatInfoCache();
	private HeartbeatInfoCache databaseInfoCache = new HeartbeatInfoCache();
	

	public void setServletContext(ServletContext servletContext)
	{
		this.servletContext = servletContext;
	}

	public void setTenantService(TenantService tenantService)
	{
		this.tenantService = tenantService;
	}

	public void setRepositoryService(RepositoryService repositoryService)
	{
		this.repositoryService = repositoryService;
	}

	public void setEngineService(EngineService engineService)
	{
		this.engineService = engineService;
	}

	public void setLocalesList(LocalesList localesList)
	{
		this.localesList = localesList;
	}

	public void setContributor(HeartbeatContributor contributor)
	{
		this.optionalContributor = contributor;
	}

	public void setDataSource(DataSource dataSource)
	{
		this.dataSource = dataSource;
	}

	public boolean getEnabled() 
	{
		return enabled;
	}

	public void setEnabled(boolean enabled) 
	{
		this.enabled = enabled;
	}

	public boolean getAskForPermission() 
	{
		return askForPermission;
	}

	public void setAskForPermission(boolean askForPermission) 
	{
		this.askForPermission = askForPermission;
	}

	public boolean getPermissionGranted() 
	{
		return permissionGranted;
	}

	public void setPermissionGranted(boolean permissionGranted) 
	{
		this.permissionGranted = permissionGranted;
	}

	public String getUrl() 
	{
		return url;
	}

	public void setUrl(String url) 
	{
		this.url = url;
	}

	public int getMaxCacheSize() 
	{
		return maxCacheSize;
	}

	public void setMaxCacheSize(int maxCacheSize) 
	{
		this.maxCacheSize = maxCacheSize;
	}

	public long getCacheSaveInterval() 
	{
		return cacheSaveInterval;
	}

	public void setCacheSaveInterval(long cacheSaveInterval) 
	{
		this.cacheSaveInterval = cacheSaveInterval;
	}

	public void setProductName(String productName) 
	{
		this.productName = productName;
	}

	public void setProductVersion(String productVersion) 
	{
		this.productVersion = productVersion;
	}

	public String getProductVersion() 
	{
		return productVersion;
	}

	private String getLocalId()
	{
		return localId;
	}
	
	private File getLocalIdFile()
	{
		File jsHomeDir = new File(new File(System.getProperty("user.home")), ".jasperserver");
		
		return new File(jsHomeDir, getLocalId());
	}
	
	private File getClientInfoCacheFile()
	{
		File jsHomeDir = new File(new File(System.getProperty("user.home")), ".jasperserver");
		
		return new File(jsHomeDir, getLocalId() + ".ser");
	}
	
	public void init()
	{
		if (enabled)
		{
			try
			{
				initHeartbeat();
			}
			catch(Exception e) // just protect the server from heartbeat initialization problems. 
			{
				if (log.isDebugEnabled())
					log.debug("Heartbeat initialization failed.", e);
			}
		}
	}
	
	private void initHeartbeat()
	{
		osName = System.getProperty("os.name");
		osVersion = System.getProperty("os.version");
		javaVendor = System.getProperty("java.vendor");
		javaVersion = System.getProperty("java.version");
		serverInfo = servletContext.getServerInfo();
		location = servletContext.getRealPath("/");
		dbName = null;
		dbVersion = null;
		
		Connection connection = null;

		try
		{
			connection = dataSource.getConnection();
			DatabaseMetaData metaData = connection.getMetaData();
			
			dbName = metaData.getDatabaseProductName();
			dbVersion = metaData.getDatabaseProductVersion();
		}
		catch (SQLException e)
		{
			if (log.isDebugEnabled())
				log.debug("Getting database metadata failed.", e);
		}
		finally
		{
			if (connection != null)
			{
				try
				{
					connection.close();
				}
				catch (SQLException e)
				{
				}
			}
		}
		
		String idSource = 
			//javaVendor + "|" 
			//+ javaVersion + "|" 
			serverInfo + "|" 
			+ productName + "|" 
			//+ productVersion + "|" 
			+ (location == null ? "" : location);
		
		MessageDigest messageDigest = null;
		try
		{
			messageDigest = MessageDigest.getInstance("MD5");
		}
		catch(NoSuchAlgorithmException e)
		{
			//heartbeat is always silent
		}
		
		if (messageDigest == null)
		{
			localId = String.valueOf(idSource.hashCode());
		}
		else
		{
			byte[] idBytes = messageDigest.digest(idSource.getBytes());
			StringBuffer idBuffer = new StringBuffer(2 * idBytes.length);
			for(int i = 0; i < idBytes.length; i++)
			{
				String hexa = Integer.toHexString(128 + idBytes[i]).toUpperCase();
				hexa = ("00" + hexa).substring(hexa.length());
				idBuffer.append(hexa);
			}
			localId = idBuffer.toString();
		}
		
		/*   */
		File localIdFile = getLocalIdFile();
		if (localIdFile.exists() && localIdFile.isFile())
		{
			localIdProperties = new Properties();

			FileInputStream fis = null;
			try
			{
				fis = new FileInputStream(localIdFile);
				localIdProperties.load(fis);
			}
			catch (IOException e)
			{
				if (log.isDebugEnabled())
					log.debug("Loading heartbeat local ID properties file failed.", e);
			}
			finally
			{
				if (fis != null)
				{
					try
					{
						fis.close();
					}
					catch (IOException e)
					{
					}
				}
			}
		}
		heartbeatId = localIdProperties.getProperty(PROPERTY_HEARTBEAT_ID);
		
		/*   */
		File clientInfoCacheFile = getClientInfoCacheFile();
		if (clientInfoCacheFile.exists() && clientInfoCacheFile.isFile())
		{
			try
			{
				clientInfoCache = (HeartbeatInfoCache)JRLoader.loadObject(clientInfoCacheFile);
			}
			catch (Exception e)
			{
				if (log.isDebugEnabled())
					log.debug("Loading heartbeat cache from serialized file failed.", e);
			}
		}
		lastCacheSaveTime = System.currentTimeMillis();
	}
	
	public boolean haveToAskForPermissionNow() 
	{
		return 
			enabled 
			&& getAskForPermission()
			&& (
				!localIdProperties.containsKey(PROPERTY_PERMISSION_GRANTED)
				|| (
					!Boolean.valueOf(localIdProperties.getProperty(PROPERTY_PERMISSION_GRANTED))
					&& !getProductVersion().equals(localIdProperties.getProperty(PROPERTY_VERSION))
					)
				);
	}
	
	public boolean isMakingCalls()
	{
		return
			enabled
			&& (
				(getAskForPermission() && Boolean.valueOf(localIdProperties.getProperty(PROPERTY_PERMISSION_GRANTED)).booleanValue())
				|| (!getAskForPermission() && getPermissionGranted())
				);
	}
	
	public synchronized void permitCall(boolean isCallPermitted) 
	{
		if (enabled)
		{
			localIdProperties.setProperty(PROPERTY_PERMISSION_GRANTED, String.valueOf(isCallPermitted));
			localIdProperties.setProperty(PROPERTY_VERSION, getProductVersion());
			
			if (heartbeatId != null)
			{
				//this is probably not needed since the heartbeat id is always set upon retrieval from URL
				localIdProperties.setProperty(PROPERTY_HEARTBEAT_ID, heartbeatId);
			}
			
			saveLocalIdProperties();
			
			//initial call after first login
			httpCall(getUrl(), isCallPermitted ? this : OPTOUT_HEARTBEAT_CONTRIBUTOR);
		}
	}
	
	public synchronized void updateClientInfo(HeartbeatClientInfo info)
	{
		if (isMakingCalls())
		{
			clientInfoCache.update(info);
			
			if (getMaxCacheSize() > 0 && clientInfoCache.size() > getMaxCacheSize())
			{
				httpCall(url, this);
			}
			else if (getCacheSaveInterval() > 0 && System.currentTimeMillis() - lastCacheSaveTime > getCacheSaveInterval())
			{
				saveCache();
			}
		}
	}
	
	public synchronized void call() 
	{
		if (enabled)
		{
			if (isMakingCalls())
			{
				createDatabaseInfoCache();
				
				httpCall(getUrl(), this);
			}
			else if (
				!getAskForPermission() 
				&& !getPermissionGranted() 
				&& !localIdProperties.containsKey(PROPERTY_PERMISSION_GRANTED)
				)
			{
				localIdProperties.setProperty(PROPERTY_PERMISSION_GRANTED, "false");
				saveLocalIdProperties();
				
				httpCall(getUrl(), OPTOUT_HEARTBEAT_CONTRIBUTOR);
			}
				
			callCount++;
		}
		else
		{
			if (log.isDebugEnabled())
				log.debug("Heartbeat is DISABLED.");
		}
	}
	
	private synchronized void httpCall(String url, HeartbeatContributor contributor) 
	{
		if (log.isDebugEnabled())
			log.debug("Heartbeat calling: " + url);
		
		HttpClient httpClient = new HttpClient();

		PostMethod post = new PostMethod(url);

		try 
		{
			if (heartbeatId != null)
			{
				post.addParameter("id", heartbeatId);
			}

			if (contributor != null)
			{
				contributor.contributeToHttpCall(post);
			}
			
			int statusCode = httpClient.executeMethod(post);
			if (statusCode == HttpStatus.SC_OK)
			{
				if (heartbeatId == null)
				{
					heartbeatId = post.getResponseBodyAsString();
					heartbeatId = heartbeatId == null ? null : heartbeatId.trim();
					
					localIdProperties.setProperty(PROPERTY_HEARTBEAT_ID, heartbeatId);

					saveLocalIdProperties();
				}
			}
			else if ( 
				//supported types of redirect
				statusCode == HttpStatus.SC_MOVED_PERMANENTLY
				|| statusCode == HttpStatus.SC_MOVED_TEMPORARILY
				|| statusCode == HttpStatus.SC_SEE_OTHER
				|| statusCode == HttpStatus.SC_TEMPORARY_REDIRECT
				)
			{
				Header header = post.getResponseHeader("location");
				if (header != null)
				{
					if (log.isDebugEnabled())
						log.debug("Heartbeat listener redirected.");

					httpCall(header.getValue(), contributor);
				}
				else
				{
					if (log.isDebugEnabled())
						log.debug("Heartbeat listener redirected to unknown destination.");
				}
			}
			else
			{
				if (log.isDebugEnabled())
					log.debug("Connecting to heartbeat listener URL failed. Status code: " + statusCode);
			}
		}
		catch (IOException e)
		{
			if (log.isDebugEnabled())
				log.debug("Connecting to heartbeat listener URL failed.", e);
		}
		finally
		{
			// Release current connection to the connection pool once you are done
			post.releaseConnection();
			
			clearCache();
		}
	}
	
	public void contributeToHttpCall(PostMethod post)
	{
		post.addParameter("callCount", String.valueOf(callCount));
		post.addParameter("osName", osName);
		post.addParameter("osVersion", osVersion);
		post.addParameter("javaVendor", javaVendor);
		post.addParameter("javaVersion", javaVersion);
		post.addParameter("serverInfo", serverInfo);
		post.addParameter("productName", productName);
		post.addParameter("productVersion", productVersion);
		post.addParameter("dbName", dbName);
		post.addParameter("dbVersion", dbVersion);
		post.addParameter("serverLocale", Locale.getDefault().toString());

		try
		{
			post.addParameter("tenants", String.valueOf(tenantService.getNumberOfTenants(null)));
		}
		catch (Exception e)
		{
			if (log.isDebugEnabled())
				log.debug("Getting number of tenants failed.", e);
		}
		
		try
		{
			UserLocale[] userLocales = localesList.getUserLocales(Locale.getDefault());
			if (userLocales != null && userLocales.length > 0)
			{
				StringBuffer sbuffer = new StringBuffer();
				for(int i = 0; i < userLocales.length; i++)
				{
					sbuffer.append(", " + userLocales[i].getCode());
				}
				post.addParameter("userLocales", sbuffer.substring(2));
			}
		}
		catch (Exception e)
		{
			if (log.isDebugEnabled())
				log.debug("Getting user locales failed.", e);
		}
		
		clientInfoCache.contributeToHttpCall(post);

		databaseInfoCache.contributeToHttpCall(post);
		
		if (optionalContributor != null)
		{
			optionalContributor.contributeToHttpCall(post);
		}
	}
	
	public void createDatabaseInfoCache()
	{
		databaseInfoCache = new HeartbeatInfoCache();
		
		List dataSources = new ArrayList();
		
		try
		{
			List jdbcDataSources = 
				repositoryService.loadClientResources(
					FilterCriteria.createFilter(JdbcReportDataSource.class)
					);
			if (jdbcDataSources != null)
				dataSources.addAll(jdbcDataSources);
		}
		catch (Exception e)
		{
			if (log.isDebugEnabled())
				log.debug("Getting JDBC data sources list failed.", e);
		}
			
		try
		{
			List jndiDataSources = 
				repositoryService.loadClientResources(
					FilterCriteria.createFilter(JndiJdbcReportDataSource.class)
					);
			if (jndiDataSources != null)
				dataSources.addAll(jndiDataSources);
		}
		catch (Exception e)
		{
			if (log.isDebugEnabled())
				log.debug("Getting JNDI data sources list failed.", e);
		}

		for(Iterator it = dataSources.iterator(); it.hasNext();)
		{
			ReportDataSource dataSource = (ReportDataSource)it.next();

			Map paramValues = new HashMap();
			
			try
			{
				ReportDataSourceService dataSourceService = engineService.createDataSourceService(dataSource);
				dataSourceService.setReportParameterValues(paramValues);
			}
			catch(Exception e)
			{
				if (log.isDebugEnabled())
					log.debug("Getting connection to data source failed.", e);
			}
			
			Connection connection = (Connection)paramValues.get(JRParameter.REPORT_CONNECTION);
			if (connection != null)
			{
				try
				{
					DatabaseMetaData metaData = connection.getMetaData();
					
					HeartbeatDatabaseInfo dbInfo = new HeartbeatDatabaseInfo();
					dbInfo.setDatabaseName(metaData.getDatabaseProductName());
					dbInfo.setDatabaseVersion(metaData.getDatabaseProductVersion());
					databaseInfoCache.update(dbInfo);
				}
				catch (SQLException e)
				{
					if (log.isDebugEnabled())
						log.debug("Getting database metadata failed.", e);
				}
				finally
				{
					if (connection != null)
					{
						try
						{
							connection.close();
						}
						catch (SQLException e)
						{
						}
					}
				}
			}
		}
	}
	
	private synchronized void saveLocalIdProperties() 
	{
		File localIdFile = getLocalIdFile();
		if (!localIdFile.exists())
		{
			localIdFile.getParentFile().mkdirs();
		}
		
		FileOutputStream fos = null;
		
		try
		{
			fos = new FileOutputStream(localIdFile);
			localIdProperties.store(fos, "heartbeat local ID file");
			fos.flush();
		}
		catch (IOException e)
		{
			if (log.isDebugEnabled())
				log.debug("Creating heartbeat local ID properties file failed.", e);
		}
		finally
		{
			if (fos != null)
			{
				try
				{
					fos.close();
				}
				catch (IOException e)
				{
				}
			}
		}
	}
	
	private synchronized void saveCache() 
	{
		File clientInfoCacheFile = getClientInfoCacheFile();
		if (!clientInfoCacheFile.exists())
		{
			clientInfoCacheFile.getParentFile().mkdirs();
		}
		
		try
		{
			JRSaver.saveObject(clientInfoCache, clientInfoCacheFile);
			lastCacheSaveTime = System.currentTimeMillis();
		}
		catch (JRException e)
		{
			if (log.isDebugEnabled())
				log.debug("Saving heartbeat cache failed.", e);
		}
	}
	
	private synchronized void clearCache() 
	{
		clientInfoCache = new HeartbeatInfoCache();
		getClientInfoCacheFile().delete();
		lastCacheSaveTime = System.currentTimeMillis();
	}
	
}
