/**
 * $URL$
 * $Id$
 *
 * Copyright (c) 2006-2009 The Sakai Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.sitestats.impl.report;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.MimeConstants;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.util.WorkbookUtil;
import org.hibernate.Criteria;
import org.hibernate.criterion.Expression;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate5.HibernateCallback;
import org.springframework.orm.hibernate5.support.HibernateDaoSupport;

import org.sakaiproject.content.api.ContentCollection;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.event.api.EventTrackingService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.javax.PagingPosition;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.sitestats.api.EventStat;
import org.sakaiproject.sitestats.api.ResourceStat;
import org.sakaiproject.sitestats.api.SitePresence;
import org.sakaiproject.sitestats.api.SiteVisits;
import org.sakaiproject.sitestats.api.Stat;
import org.sakaiproject.sitestats.api.StatsAuthz;
import org.sakaiproject.sitestats.api.StatsManager;
import org.sakaiproject.sitestats.api.Util;
import org.sakaiproject.sitestats.api.event.EventInfo;
import org.sakaiproject.sitestats.api.event.EventRegistryService;
import org.sakaiproject.sitestats.api.event.ToolInfo;
import org.sakaiproject.sitestats.api.report.Report;
import org.sakaiproject.sitestats.api.report.ReportDef;
import org.sakaiproject.sitestats.api.report.ReportFormattedParams;
import org.sakaiproject.sitestats.api.report.ReportManager;
import org.sakaiproject.sitestats.api.report.ReportParams;
import org.sakaiproject.sitestats.impl.parser.DigesterUtil;
import org.sakaiproject.sitestats.impl.report.fop.LibraryURIResolver;
import org.sakaiproject.sitestats.impl.report.fop.ReportInputSource;
import org.sakaiproject.sitestats.impl.report.fop.ReportXMLReader;
import org.sakaiproject.time.api.UserTimeService;
import org.sakaiproject.tool.api.Placement;
import org.sakaiproject.tool.api.ToolManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.util.ResourceLoader;

/**
 * @author Nuno Fernandes
 *
 */
@Slf4j
public class ReportManagerImpl extends HibernateDaoSupport implements ReportManager, Observer {

	private ReportFormattedParams	formattedParams	= new ReportFormattedParamsImpl();

	/** FOP */
	private FopFactory				fopFactory		= FopFactory.newInstance();
	private Templates				cachedXmlFoXSLT	= null;
	private static final String		XML_FO_XSL_FILE	= "xmlReportToFo.xsl";

	/** Date formatters. */
	private SimpleDateFormat		dateMonthFrmt 	= new SimpleDateFormat("yyyy-MM");
	private SimpleDateFormat		dateYearFrmt  	= new SimpleDateFormat("yyyy");

	/** Sakai services */
	@Setter private ResourceLoader resourceLoader;
	@Setter private StatsManager statsManager;
	@Setter private StatsAuthz statsAuthz;
	@Setter private EventRegistryService eventRegistryService;
	@Setter private SiteService siteService;
	@Setter private UserDirectoryService userDirectoryService;
	@Setter private ContentHostingService contentHostingService;
	@Setter private ToolManager toolManager;
	@Setter private UserTimeService userTimeService;
	@Setter private EventTrackingService eventTrackingService;
	@Setter private MemoryService memoryService;
	
	/** Caching */
	private Cache<String, Object> cacheReportDef = null;
	
	public void init(){
		boolean testsEnabled = BooleanUtils.toBoolean(System.getProperty("sakai.tests.enabled"));
		if (!testsEnabled) {
			resourceLoader = new ResourceLoader("Messages");
		}

		// Initialize cacheReportDef and event observer for cacheReportDef invalidation across cluster
		eventTrackingService.addPriorityObserver(this);
		cacheReportDef = memoryService.getCache(ReportDef.class.getName());
	}
	
	public void destroy(){
		eventTrackingService.deleteObserver(this);
	}

	/** EventTrackingService observer for cache invalidation. */
	public void update(Observable obs, Object o) {
		if(o instanceof Event){
			Event e = (Event) o;
			String prefix = StatsManager.LOG_APP + '.' + StatsManager.LOG_OBJ_REPORTDEF;
			if(e.getEvent() != null 
					&& e.getEvent().startsWith(prefix)
					&& (e.getEvent().endsWith(StatsManager.LOG_ACTION_NEW)
						|| e.getEvent().endsWith(StatsManager.LOG_ACTION_EDIT)
						|| e.getEvent().endsWith(StatsManager.LOG_ACTION_DELETE)
						)
				) {
				String[] parts = e.getResource().split("/");
				String id = parts[4];
				String siteId = parts[2];
				
				// expire report with specified id
				log.debug("Expiring report for id: "+siteId);
				cacheReportDef.remove(id);
				
				// expire list of site reports
				log.debug("Expiring report lists for site: "+siteId);
				cacheReportDef.remove( new KeyReportDefList(siteId, true, true).toString() );
				cacheReportDef.remove( new KeyReportDefList(siteId, true, false).toString() );
				cacheReportDef.remove( new KeyReportDefList(siteId, false, true).toString() );
				cacheReportDef.remove( new KeyReportDefList(siteId, false, false).toString() );

				// expire list of predefined reports
				// required as event contains siteId and not null (which identifies predefined reports)
				log.debug("Expiring predefined report lists");
				cacheReportDef.remove( new KeyReportDefList(null, true, true).toString() );
				cacheReportDef.remove( new KeyReportDefList(null, true, false).toString() );
				cacheReportDef.remove( new KeyReportDefList(null, false, true).toString() );
				cacheReportDef.remove( new KeyReportDefList(null, false, false).toString() );
			}
		}
	}
	

	// ################################################################
	// Interface implementation
	// ################################################################
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReport(org.sakaiproject.sitestats.api.report.ReportDef, boolean)
	 */
	public Report getReport(ReportDef reportDef, boolean restrictToToolsInSite) {
		return getReport(reportDef, restrictToToolsInSite, null, true);
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReportRowCount(org.sakaiproject.sitestats.api.report.ReportDef, boolean)
	 */
	public int getReportRowCount(ReportDef reportDef, boolean restrictToToolsInSite) {
		ReportProcessedParams rpp = processReportParams(reportDef.getReportParams(), restrictToToolsInSite, null);
		if(reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_RESOURCES)){
			return statsManager.getResourceStatsRowCount(rpp.siteId, rpp.resourceAction, rpp.resourceIds, rpp.iDate, rpp.fDate, rpp.userIds, rpp.inverseUserSelection, rpp.totalsBy);
		}else{
			return statsManager.getEventStatsRowCount(rpp.siteId, rpp.events, rpp.iDate, rpp.fDate, rpp.userIds, rpp.inverseUserSelection, rpp.totalsBy);
		}
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReport(org.sakaiproject.sitestats.api.report.ReportDef, boolean, org.sakaiproject.javax.PagingPosition, boolean)
	 */
	public Report getReport(ReportDef reportDef, boolean restrictToToolsInSite, PagingPosition pagingPosition, boolean log) {
		ReportProcessedParams rpp = processReportParams(reportDef.getReportParams(), restrictToToolsInSite, pagingPosition);

		// generate report
		Report report = new Report();
		List<Stat> data = null;
		if (reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_RESOURCES)) {
			data = statsManager.getResourceStats(rpp.siteId, rpp.resourceAction, rpp.resourceIds, rpp.iDate, rpp.fDate, rpp.userIds, rpp.inverseUserSelection, pagingPosition, rpp.totalsBy, rpp.sortBy, rpp.sortAscending, rpp.maxResults);
		} else if(reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_VISITS)
				|| reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_EVENTS)) {
			data = statsManager.getEventStats(rpp.siteId, rpp.events, rpp.iDate, rpp.fDate, rpp.userIds, rpp.inverseUserSelection, pagingPosition, rpp.totalsBy, rpp.sortBy, rpp.sortAscending, rpp.maxResults);
		} else if(reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_PRESENCES)) {
			data = statsManager.getPresenceStats(rpp.siteId, rpp.iDate, rpp.fDate, rpp.userIds, rpp.inverseUserSelection, pagingPosition, rpp.totalsBy, rpp.sortBy, rpp.sortAscending, rpp.maxResults);
		} else if(reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_VISITS_TOTALS)) {
			data = statsManager.getVisitsTotalsStats(rpp.siteId, rpp.iDate, rpp.fDate, pagingPosition, rpp.totalsBy, rpp.sortBy, rpp.sortAscending, rpp.maxResults);
		} else if(reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_ACTIVITY_TOTALS)) {
			data = statsManager.getActivityTotalsStats(rpp.siteId, rpp.events, rpp.iDate, rpp.fDate, pagingPosition, rpp.totalsBy, rpp.sortBy, rpp.sortAscending, rpp.maxResults);
		} else if (reportDef.getReportParams().getWhat().equals(ReportManager.WHAT_LESSONPAGES)) {
			data = statsManager.getLessonBuilderStats(rpp.siteId, rpp.resourceAction, rpp.resourceIds, rpp.iDate, rpp.fDate, rpp.userIds, rpp.inverseUserSelection, pagingPosition, rpp.totalsBy, rpp.sortBy, rpp.sortAscending, rpp.maxResults);
		}
		
		// add missing info in report and its parameters
		reportDef.getReportParams().setWhenFrom(rpp.iDate);
		reportDef.getReportParams().setWhenTo(rpp.fDate);
		reportDef.getReportParams().setWhoUserIds(rpp.userIds);
		reportDef.getReportParams().setHowTotalsBy(rpp.totalsBy);
		report.setReportData(data);
		report.setReportDefinition(reportDef);
		report.setReportGenerationDate(new Date());
		if (log && reportDef.getId() != 0) {
			String siteId = reportDef.getSiteId();
			if (siteId == null) {
				siteId = reportDef.getReportParams().getSiteId();
			}
			statsManager.logEvent(reportDef, StatsManager.LOG_ACTION_VIEW, siteId, true);
		}

		return report;
	}
	
	private ReportProcessedParams processReportParams(ReportParams params, boolean restrictToToolsInSite, PagingPosition pagingPosition) {
		ReportProcessedParams rpp = new ReportProcessedParams();
		
		// site
		rpp.siteId = params.getSiteId();
		
		// what (visits, events, resources)
		rpp.events = new ArrayList<String>();
		if(params.getWhat().equals(ReportManager.WHAT_VISITS)){
			rpp.events.add(StatsManager.SITEVISIT_EVENTID);

		}else if(params.getWhat().equals(ReportManager.WHAT_EVENTS)){
			if(params.getWhatEventSelType().equals(ReportManager.WHAT_EVENTS_BYTOOL)){
				Iterator<ToolInfo> iT = null;
				if(rpp.siteId != null) {
					iT = eventRegistryService.getEventRegistry(rpp.siteId, restrictToToolsInSite).iterator();
				}else{
					iT = eventRegistryService.getEventRegistry().iterator();
				}
				while (iT.hasNext()){
					ToolInfo t = iT.next();
					if(params.getWhatToolIds().contains(t.getToolId())
							|| params.getWhatToolIds().contains(WHAT_EVENTS_ALLTOOLS)){
						Iterator<EventInfo> iE = t.getEvents().iterator();
						while (iE.hasNext())
							rpp.events.add(iE.next().getEventId());
					}
				}
			}else{
				List<String> eventIds = params.getWhatEventIds();
				if(eventIds != null) {
					rpp.events.addAll(eventIds);
				}
			}

		}else if(params.getWhat().equals(ReportManager.WHAT_RESOURCES)){
			rpp.resourceIds = null;
			rpp.resourceAction = null;
			if(params.isWhatLimitedResourceIds() && params.getWhatResourceIds() != null){
				rpp.resourceIds = new ArrayList<String>();
				Iterator<String> iR = params.getWhatResourceIds().iterator();
				while (iR.hasNext()) {
					String next = iR.next();
					rpp.resourceIds.add("/content" + next);
				}
			}
			if(params.isWhatLimitedAction() && params.getWhatResourceAction() != null) {
				rpp.resourceAction = params.getWhatResourceAction();			
			}
		} else if (params.getWhat().equals(ReportManager.WHAT_LESSONPAGES)){
			rpp.resourceIds = null;
			rpp.resourceAction = null;
			if (params.isWhatLimitedResourceIds() && params.getWhatResourceIds() != null) {
				rpp.resourceIds = new ArrayList<String>();
				for (String iR : params.getWhatResourceIds()) {
					rpp.resourceIds.add("/lessonbuilder" + iR);
				}
			}
			if (params.isWhatLimitedAction() && params.getWhatResourceAction() != null) {
				rpp.resourceAction = params.getWhatResourceAction();			
			}
		}

		// when (dates)
		rpp.fDate = null;
		rpp.iDate = null;
		if(params.getWhen().equals(ReportManager.WHEN_CUSTOM)){
			rpp.iDate = params.getWhenFrom();
			rpp.fDate = params.getWhenTo();
		}else rpp.fDate = new Date();
		if(params.getWhen().equals(ReportManager.WHEN_ALL)){
			if(rpp.siteId != null) {
                // ADRIAN
				rpp.iDate = statsManager.getInitialActivityDate(rpp.siteId);
			}else{
				rpp.iDate = null;
			}
		}else if(params.getWhen().equals(ReportManager.WHEN_LAST7DAYS)){
			Calendar c = Calendar.getInstance();
			c.set(Calendar.HOUR_OF_DAY, 00);
			c.set(Calendar.MINUTE, 00);
			c.set(Calendar.SECOND, 00);
			c.add(Calendar.DATE, -6);
			rpp.iDate = c.getTime();
		}else if(params.getWhen().equals(ReportManager.WHEN_LAST30DAYS)){
			Calendar c = Calendar.getInstance();
			c.set(Calendar.HOUR_OF_DAY, 00);
			c.set(Calendar.MINUTE, 00);
			c.set(Calendar.SECOND, 00);
			c.add(Calendar.DATE, -29);
			rpp.iDate = c.getTime();
		}else if(params.getWhen().equals(ReportManager.WHEN_LAST365DAYS)){
			Calendar c = Calendar.getInstance();
			c.set(Calendar.HOUR_OF_DAY, 00);
			c.set(Calendar.MINUTE, 00);
			c.set(Calendar.SECOND, 00);
			c.add(Calendar.DATE, -364);
			rpp.iDate = c.getTime();
		}
		params.setWhenFrom(rpp.iDate);
		params.setWhenTo(rpp.fDate);

		// who (users, groups, roles)
		rpp.userIds = new ArrayList<String>();
		rpp.inverseUserSelection = false;
		if(params.getWho().equals(ReportManager.WHO_ALL)){
			try{
				Site site = siteService.getSite(rpp.siteId);
				rpp.userIds.addAll(site.getUsers());
			}catch(IdUnusedException e){
				log.error("No site with specified siteId.");
			}
		}else if(params.getWho().equals(ReportManager.WHO_ROLE) && rpp.siteId != null){
			try{
				Site site = siteService.getSite(rpp.siteId);
				rpp.userIds.addAll(site.getUsersHasRole(params.getWhoRoleId()));
			}catch(IdUnusedException e){
				log.error("No site with specified siteId.");
			}
		}else if(params.getWho().equals(ReportManager.WHO_GROUPS) && rpp.siteId != null){
			try{
				Site site = siteService.getSite(rpp.siteId);
				rpp.userIds.addAll(site.getGroup(params.getWhoGroupId()).getUsers());
			}catch(IdUnusedException e){
				log.error("No site with specified siteId.");
			}
		}else if(params.getWho().equals(ReportManager.WHO_CUSTOM)){
			rpp.userIds = params.getWhoUserIds();
		}else{
			// inverse
			rpp.inverseUserSelection = true;
			rpp.userIds = null;
		}
		params.setWhoUserIds(rpp.userIds);
		
		// how
		rpp.totalsBy = params.getHowTotalsBy();
		if(HOW_SORT_DEFAULT.equals(params.getHowSortBy())) {
			rpp.sortBy = null;
		}else{
			rpp.sortBy = params.getHowSortBy();
		}
		rpp.sortAscending = params.getHowSortAscending();
		if(params.isHowLimitedMaxResults() && params.getHowMaxResults() > 0) {
			rpp.maxResults = params.getHowMaxResults();			
		}else{
			rpp.maxResults = 0;
		}

		return rpp;
	}

	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReportFormattedParams()
	 */
	public ReportFormattedParams getReportFormattedParams() {
		return formattedParams;
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#isReportColumnAvailable(org.sakaiproject.sitestats.api.report.ReportParams, java.lang.String)
	 */
	public boolean isReportColumnAvailable(ReportParams params, String column) {
		List<String> totalsBy = params.getHowTotalsBy();
		if(column == null) {
			return false;
			
		}else if(column.equals(StatsManager.T_SITE)) {
			return totalsBy.contains(StatsManager.T_SITE);
			
		}else if(column.equals(StatsManager.T_USER)) {
			return totalsBy.contains(StatsManager.T_USER);
			
		}else if(column.equals(StatsManager.T_EVENT)) {
			return totalsBy.contains(StatsManager.T_EVENT) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_TOOL)) {
			return totalsBy.contains(StatsManager.T_TOOL) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_RESOURCE)) {
			return totalsBy.contains(StatsManager.T_RESOURCE) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_RESOURCE_ACTION)) {
			return totalsBy.contains(StatsManager.T_RESOURCE_ACTION) && !ReportManager.WHO_NONE.equals(params.getWho());

		}else if(column.equals(StatsManager.T_PAGE)) {
			return totalsBy.contains(StatsManager.T_PAGE) && !ReportManager.WHO_NONE.equals(params.getWho());

		}else if(column.equals(StatsManager.T_PAGE_ACTION)) {
			return totalsBy.contains(StatsManager.T_PAGE_ACTION) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_DATE)) {
			return totalsBy.contains(StatsManager.T_DATE) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_DATEMONTH)) {
			return totalsBy.contains(StatsManager.T_DATEMONTH) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_DATEYEAR)) {
			return totalsBy.contains(StatsManager.T_DATEYEAR) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_LASTDATE)) {
			return totalsBy.contains(StatsManager.T_LASTDATE) && !ReportManager.WHO_NONE.equals(params.getWho());
			
		}else if(column.equals(StatsManager.T_TOTAL)) {
			return !ReportManager.WHAT_PRESENCES.equals(params.getWhat()) && !totalsBy.contains(StatsManager.T_DURATION)
				&& !ReportManager.WHO_NONE.equals(params.getWho()) && !ReportManager.WHAT_VISITS_TOTALS.equals(params.getWhat())
				&& !totalsBy.contains(StatsManager.T_VISITS) && !totalsBy.contains(StatsManager.T_UNIQUEVISITS);
			
		}else if(column.equals(StatsManager.T_VISITS)) {
			return totalsBy.contains(StatsManager.T_VISITS);
			
		}else if(column.equals(StatsManager.T_UNIQUEVISITS)) {
			return totalsBy.contains(StatsManager.T_UNIQUEVISITS);
			
		}else if(column.equals(StatsManager.T_DURATION)) {
			//return totalsBy.contains(StatsManager.T_DURATION);
			return ReportManager.WHAT_PRESENCES.equals(params.getWhat());
			
		}else{
			log.warn("isReportColumnAvailable(): invalid column: "+column);
			return false;
		}
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReportDefinition(long)
	 */
	public ReportDef getReportDefinition(final long id) {
		ReportDef reportDef = null;
		Object cached = cacheReportDef.get(String.valueOf(id));
		if(cached != null){
			reportDef = (ReportDef) cached;
		}else{
			HibernateCallback<ReportDef> hcb = session -> (ReportDef) session.load(ReportDef.class, Long.valueOf(id));
			Object o;
			try{
				o = getHibernateTemplate().execute(hcb);
			}catch(DataAccessException e){
				o = null;
			}
			if(o != null) {
				reportDef = (ReportDef) o;
				cacheReportDef.put(String.valueOf(id), reportDef);
			}
		}
		try{
			if(reportDef != null)
				reportDef.setReportParams(DigesterUtil.convertXmlToReportParams(reportDef.getReportDefinitionXml()));
		}catch(Exception e){
			log.warn("getReportDefinition(): unable to parse report parameters.");
		}
		return reportDef;
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#saveReportDefinition(org.sakaiproject.sitestats.api.report.ReportDef)
	 */
	public boolean saveReportDefinition(final ReportDef reportDef) {
		if(reportDef.getSiteId() == null && !statsAuthz.isSiteStatsAdminPage()) {
			return false;
		}
		boolean isNew = reportDef.getId() == 0;
		try{
			if(reportDef.getCreatedBy() == null) {
				reportDef.setCreatedBy(userDirectoryService.getCurrentUser().getId());
			}
			if(reportDef.getCreatedOn() == null) {
				reportDef.setCreatedOn(new Date());
			}
			if(reportDef.getModifiedBy() == null) {
				reportDef.setModifiedBy(userDirectoryService.getCurrentUser().getId());
			}
			if(reportDef.getModifiedOn() == null) {
				reportDef.setModifiedOn(new Date());
			}

            ReportParams params = reportDef.getReportParams();

            boolean isResourcesReport = params.getWhat().equals(ReportManager.WHAT_RESOURCES);

            boolean isResourceEvent = false;

            for (String eventId : params.getWhatEventIds()) {
                if (eventId.equals(ReportManager.WHAT_RESOURCES_ACTION_NEW)
                    || eventId.equals(ReportManager.WHAT_RESOURCES_ACTION_READ)
                    || eventId.equals(ReportManager.WHAT_RESOURCES_ACTION_REVS)
                    || eventId.equals(ReportManager.WHAT_RESOURCES_ACTION_DEL)
                    || eventId.equals(ReportManager.WHAT_RESOURCES_ACTION_DOW)) {
                    isResourceEvent = true;
                }
            }

            if (!isResourcesReport && !isResourceEvent) {
                // This has nothing to do with resources, so null the whatResourceAction
                params.setWhatResourceAction(null);
            } else {
                params.setWhatToolIds(Arrays.asList("sakai.resources"));
            }

			reportDef.setReportDefinitionXml(DigesterUtil.convertReportParamsToXml(params));
		}catch(Exception e) {
			log.warn("saveReportDefinition(): unable to generate xml string from report parameters.", e);
			return false;
		}

		try {
			getHibernateTemplate().saveOrUpdate(reportDef);
			cacheReportDef.clear();
			String siteId = reportDef.getSiteId();
			if (siteId == null) {
				siteId = reportDef.getReportParams().getSiteId();
			}
			statsManager.logEvent(reportDef, isNew ? StatsManager.LOG_ACTION_NEW : StatsManager.LOG_ACTION_EDIT, siteId, false);
			return true;
		} catch (DataAccessException dae) {
			log.error("Could not save report definition: {}", dae.getMessage(), dae);
		}
		return false;
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#removeReportDefinition(org.sakaiproject.sitestats.api.report.ReportDef)
	 */
	public boolean removeReportDefinition(final ReportDef reportDef) {
		ReportDef persistedReportDef = getHibernateTemplate().get(ReportDef.class, reportDef.getId());
		if (persistedReportDef != null) {
			getHibernateTemplate().delete(persistedReportDef);
			cacheReportDef.clear();
			String siteId = persistedReportDef.getSiteId();
			if (siteId == null) {
				siteId = persistedReportDef.getReportParams().getSiteId();
			}
			statsManager.logEvent(persistedReportDef, StatsManager.LOG_ACTION_DELETE, siteId, false);
			return true;
		}
		return false;
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReportDefinitions(java.lang.String, boolean, boolean)
	 */
	public List<ReportDef> getReportDefinitions(final String siteId, final boolean includedPredefined, final boolean includeHidden) {
		List<ReportDef> reportDefs = null;
		KeyReportDefList key = new KeyReportDefList(siteId, includedPredefined, includeHidden);
		Object cached = cacheReportDef.get(key.toString());
		if(cached != null) {
			reportDefs = (List<ReportDef>) cached;
			log.debug("Getting report list from cache for site "+siteId);
		}else{
			HibernateCallback<List<ReportDef>> hcb = session -> {
                Criteria c = session.createCriteria(ReportDef.class);
                if(siteId != null) {
                    if(includedPredefined) {
                        c.add(Expression.or(Expression.eq("siteId", siteId), Expression.isNull("siteId")));
                    }else{
                        c.add(Expression.eq("siteId", siteId));
                    }
                }else{
                    c.add(Expression.isNull("siteId"));
                }
                if(!includeHidden) {
                    c.add(Expression.eq("hidden", false));
                }
                return c.list();
            };
			reportDefs = getHibernateTemplate().execute(hcb);
			if(reportDefs != null) {
				for(ReportDef reportDef : reportDefs) {
					try{
						reportDef.setReportParams(DigesterUtil.convertXmlToReportParams(reportDef.getReportDefinitionXml()));
					}catch(Exception e){
						log.warn("getReportDefinition(): unable to parse report parameters.");
						reportDef.setReportParams(null);
					}
				}
				cacheReportDef.put(key.toString(), reportDefs);
			}
		}
		return reportDefs;
	}

	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReportAsExcel(org.sakaiproject.sitestats.api.report.Report, java.lang.String)
	 */
	public byte[] getReportAsExcel(Report report, String sheetName) {
		List<Stat> statsObjects = report.getReportData();
		HSSFWorkbook wb = new HSSFWorkbook();
		HSSFSheet sheet = wb.createSheet(WorkbookUtil.createSafeSheetName(sheetName));
		HSSFRow headerRow = sheet.createRow(0);
		
		// Add the column headers
		int ix = 0;
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_SITE)){
            headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_site"));
        }
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_USER)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_id"));
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_user"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOOL)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_tool"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_EVENT)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_event"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_resource"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE_ACTION)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_action"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATE)
			|| isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEMONTH)
			|| isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEYEAR)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_date"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_LASTDATE)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_lastdate"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOTAL)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_total"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_VISITS)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_visits"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_UNIQUEVISITS)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_uniquevisitors"));
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DURATION)) {
			headerRow.createCell(ix++).setCellValue(resourceLoader.getString("th_duration") + " (" + resourceLoader.getString("minutes_abbr") + ")");
		}

		// Fill the spreadsheet cells
		Iterator<Stat> i = statsObjects.iterator();
		while (i.hasNext()){
			HSSFRow row = sheet.createRow(sheet.getLastRowNum() + 1);
			Stat se = i.next();
			ix = 0;
			if (isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_SITE)) {
                try {
                    Site site = siteService.getSite(se.getSiteId());
                    row.createCell(ix++).setCellValue(site.getTitle());
                } catch (IdUnusedException e) {
                    logger.debug("can't find site with id: " + se.getSiteId());
                    row.createCell(ix++).setCellValue(se.getSiteId().toString());
                }
            }
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_USER)) {
				String userId = se.getUserId();
				String userEid = null;
				String userName = null;
				if (userId != null) {
	    			if(("-").equals(userId)) {
	    				userEid = "-";
	    				userName = resourceLoader.getString("user_anonymous");
	    			}else if(EventTrackingService.UNKNOWN_USER.equals(userId)) {
	    				userEid = "-";
	    				userName = resourceLoader.getString("user_anonymous_access");
	    			}else{
	    				try{
	    					User user = userDirectoryService.getUser(userId);
	    					userEid = user.getDisplayId();
	    					userName = statsManager.getUserNameForDisplay(user);
	    				}catch(UserNotDefinedException e1){
	    					userEid = userId;
	    					userName = resourceLoader.getString("user_unknown");
	    				}
	    			}
	    		}else{
	    			userName = resourceLoader.getString("user_unknown");
	    		}
				row.createCell(ix++).setCellValue(userEid);
				row.createCell(ix++).setCellValue(userName);
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOOL)) {
				EventStat es = (EventStat) se;
				row.createCell(ix++).setCellValue(eventRegistryService.getToolName(es.getToolId()));
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_EVENT)) {
				EventStat es = (EventStat) se;
				row.createCell(ix++).setCellValue(eventRegistryService.getEventName(es.getEventId()));
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE)) {
				ResourceStat rs = (ResourceStat) se;
				row.createCell(ix++).setCellValue(rs.getResourceRef());		
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE_ACTION)) {
				ResourceStat rs = (ResourceStat) se;
				row.createCell(ix++).setCellValue(rs.getResourceAction());			
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATE)) {
				java.sql.Date sqlDate = (java.sql.Date) se.getDate();
				row.createCell(ix++).setCellValue(userTimeService.shortLocalizedDate(sqlDate.toLocalDate(), resourceLoader.getLocale()));
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEMONTH)) {
				row.createCell(ix++).setCellValue(dateMonthFrmt.format(se.getDate()));			
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEYEAR)) {
				row.createCell(ix++).setCellValue(dateYearFrmt.format(se.getDate()));		
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_LASTDATE)) {
				java.sql.Date sqlDate = (java.sql.Date) se.getDate();
				row.createCell(ix++).setCellValue(userTimeService.shortLocalizedDate(sqlDate.toLocalDate(), resourceLoader.getLocale()));
			}
            if(report.getReportDefinition().getReportParams().getSiteId() != null && !"".equals(report.getReportDefinition().getReportParams().getSiteId())) {
            }
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOTAL)) {
				row.createCell(ix++).setCellValue(se.getCount());		
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_VISITS)) {
				SiteVisits sv = (SiteVisits) se;
				row.createCell(ix++).setCellValue(sv.getTotalVisits());
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_UNIQUEVISITS)) {
				SiteVisits sv = (SiteVisits) se;
				row.createCell(ix++).setCellValue(sv.getTotalUnique());
			}
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DURATION)) {
				SitePresence ss = (SitePresence) se;
				double durationInMin = ss.getDuration() == 0 ? 0 : Util.round((double)ss.getDuration() / 1000 / 60, 1); // in minutes
				row.createCell(ix++).setCellValue(durationInMin);
			}
		}

		ByteArrayOutputStream baos = null;
		try{
			baos = new ByteArrayOutputStream();
			wb.write(baos);
		}catch(IOException e){
			log.error("Error writing Excel bytes from SiteStats report", e);
		}finally{
			if(baos != null) {
				try{ baos.close(); }catch(IOException e){ /* ignore */ }
			}
		}
		if(baos != null) {
			return baos.toByteArray();
		}else{
			return new byte[0];
		}
	}

	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReportAsCsv(org.sakaiproject.sitestats.api.report.Report)
	 */
	public String getReportAsCsv(Report report) {
		List<Stat> statsObjects = report.getReportData();
		StringBuilder sb = new StringBuilder();
		boolean isFirst = true;

		// Add the headers
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_SITE)){
            appendQuoted(sb, resourceLoader.getString("th_site"));
            isFirst = false;
        }
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_USER)) {
			 if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_id"));
			sb.append(",");
			appendQuoted(sb, resourceLoader.getString("th_user"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOOL)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_tool"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_EVENT)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_event"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_resource"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE_ACTION)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_action"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATE)
			|| isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEMONTH)
			|| isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEYEAR)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_date"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_LASTDATE)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_lastdate"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOTAL)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_total"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_VISITS)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_visits"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_UNIQUEVISITS)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_uniquevisitors"));
			isFirst = false;
		}
		if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DURATION)) {
			if(!isFirst) {
				sb.append(",");
			}
			appendQuoted(sb, resourceLoader.getString("th_duration") + " (" + resourceLoader.getString("minutes_abbr") + ")");
			isFirst = false;
		}
		sb.append("\n");

		// Add the data
		Iterator<Stat> i = statsObjects.iterator();
		while (i.hasNext()){
			Stat se = i.next();
			isFirst = true;
			//site
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_SITE)){
                try {
                    Site site = siteService.getSite(se.getSiteId());
                    appendQuoted(sb, site.getTitle());
                } catch (IdUnusedException e) {
                    logger.debug("can't find site with id: " +se.getSiteId());
                    appendQuoted(sb, se.getSiteId());
                }
                isFirst=false;
            }
			// user
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_USER)) {
				if(!isFirst) {
					sb.append(",");
				}
				String userId = se.getUserId();
				String userEid = null;
				String userName = null;			
				if (userId != null) {
	    			if(("-").equals(userId)) {
	    				userEid = "-";
	    				userName = resourceLoader.getString("user_anonymous");
	    			}else if(EventTrackingService.UNKNOWN_USER.equals(userId)) {
	    				userEid = "-";
	    				userName = resourceLoader.getString("user_anonymous_access");
	    			}else{
	    				try{
	    					User user = userDirectoryService.getUser(userId);
	    					userEid = user.getDisplayId();
	    					userName = statsManager.getUserNameForDisplay(user);
	    				}catch(UserNotDefinedException e1){
	    					userEid = userId;
	    					userName = resourceLoader.getString("user_unknown");
	    				}
	    			}
	    		}else{
    				userEid = "-";
	    			userName = resourceLoader.getString("user_unknown");
	    		}
				appendQuoted(sb, userEid);
				sb.append(",");
				appendQuoted(sb, userName);
				isFirst = false;
			}
			// tool
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOOL)) {
				if(!isFirst) {
					sb.append(",");
				}
				EventStat es = (EventStat) se;
				appendQuoted(sb, eventRegistryService.getToolName(es.getToolId()));
				isFirst = false;
			}
			// event
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_EVENT)) {
				if(!isFirst) {
					sb.append(",");
				}
				EventStat es = (EventStat) se;
				appendQuoted(sb, eventRegistryService.getEventName(es.getEventId()));
				isFirst = false;
			}
			// resource
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE)) {
				if(!isFirst) {
					sb.append(",");
				}
				ResourceStat rs = (ResourceStat) se;
				appendQuoted(sb, rs.getResourceRef());
				isFirst = false;
			}
			// resource action
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_RESOURCE_ACTION)) {
				if(!isFirst) {
					sb.append(",");
				}
				ResourceStat rs = (ResourceStat) se;
				appendQuoted(sb, rs.getResourceAction());
				isFirst = false;
			}
			// date
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATE)) {
				if(!isFirst) {
					sb.append(",");
				}
				java.sql.Date sqlDate = (java.sql.Date) se.getDate();
				appendQuoted(sb, userTimeService.shortLocalizedDate(sqlDate.toLocalDate(), resourceLoader.getLocale()));
				isFirst = false;
			}
			// date (year-month)
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEMONTH)) {
				if(!isFirst) {
					sb.append(",");
				}
				appendQuoted(sb, dateMonthFrmt.format(se.getDate()));
				isFirst = false;
			}
			// date (year)
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DATEYEAR)) {
				if(!isFirst) {
					sb.append(",");
				}
				appendQuoted(sb, dateYearFrmt.format(se.getDate()));
				isFirst = false;
			}
			// last date
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_LASTDATE)) {
				if(!isFirst) {
					sb.append(",");
				}
				java.sql.Date sqlDate = (java.sql.Date) se.getDate();
				appendQuoted(sb, userTimeService.shortLocalizedDate(sqlDate.toLocalDate(), resourceLoader.getLocale()));
				isFirst = false;
			}
			// total
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_TOTAL)) {
				if(!isFirst) {
					sb.append(",");
				}
				appendQuoted(sb, Long.toString(se.getCount()));
				isFirst = false;
			}
			// visits
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_VISITS)) {
				if(!isFirst) {
					sb.append(",");
				}
				SiteVisits sv = (SiteVisits) se;
				appendQuoted(sb, Long.toString(sv.getTotalVisits()));
				isFirst = false;
			}
			// unique visitors
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_UNIQUEVISITS)) {
				if(!isFirst) {
					sb.append(",");
				}
				SiteVisits sv = (SiteVisits) se;
				appendQuoted(sb, Long.toString(sv.getTotalUnique()));
				isFirst = false;
			}
			// duration
			if(isReportColumnAvailable(report.getReportDefinition().getReportParams(), StatsManager.T_DURATION)) {
				if(!isFirst) {
					sb.append(",");
				}
				SitePresence ss = (SitePresence) se;
				double durationInMin = ss.getDuration() == 0 ? 0 : Util.round((double)ss.getDuration() / 1000 / 60, 1); // in minutes
				appendQuoted(sb, Double.toString(durationInMin));
				isFirst = false;
			}
			sb.append("\n");
		}
		return sb.toString();
	}
	
	/* (non-Javadoc)
	 * @see org.sakaiproject.sitestats.api.report.ReportManager#getReportAsPDF(org.sakaiproject.sitestats.api.report.Report)
	 */
	public byte[] getReportAsPDF(Report report) {
		ByteArrayOutputStream out = null;
		try{
			// Setup a buffer to obtain the content length
		    out = new ByteArrayOutputStream();		    
		    fopFactory.setURIResolver(new LibraryURIResolver());			
		    FOUserAgent foUserAgent = fopFactory.newFOUserAgent();			
			
            // Construct fop with desired output format
            Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out);

            // Setup XSLT
            if(cachedXmlFoXSLT == null) {            	
            	ClassPathResource xsltCPR = new ClassPathResource("org/sakaiproject/sitestats/config/fop/"+XML_FO_XSL_FILE);
            	InputStream xslt = xsltCPR.getInputStream();
            	TransformerFactory factory = TransformerFactory.newInstance();
	            cachedXmlFoXSLT = factory.newTemplates(new StreamSource(xslt));
            }
            Transformer transformer = cachedXmlFoXSLT.newTransformer();
        
            // Setup input for XSLT transformation
            Source src = new SAXSource(new ReportXMLReader(), new ReportInputSource(report));
        
            // Resulting SAX events (the generated FO) must be piped through to FOP
            Result res = new SAXResult(fop.getDefaultHandler());

            // Start XSLT transformation and FOP processing
            transformer.transform(src, res);
    	    
		}catch(TransformerConfigurationException e){
			log.error("TransformerConfigurationException while writing SiteStats PDF report", e);
		}catch(FOPException e){
			log.error("FOPException while writing SiteStats PDF report", e);
		}catch(TransformerException e){
			log.error("TransformerException while writing SiteStats PDF report", e);
		}catch(Exception e){
			log.error("Exception while generating SiteStats PDF report", e);
		}finally{
			try{
				if(out != null) {
					out.close();
					return out.toByteArray();
				}
			}catch(IOException e){
				log.error("IOException while writing SiteStats PDF report", e);
			}
		}
		return null;
	}
	

	// ################################################################
	// Utility methods
	// ################################################################

	private Report consolidateAnonymousEvents(Report report) {
		List<Stat> consolidated = new ArrayList<Stat>();
		List<Stat> list = report.getReportData();
		Map<String, Stat> anonMap = new HashMap<String, Stat>();

		for(Stat s : list){
			if(s instanceof EventStat) {
				EventStat es = (EventStat) s;
				String eventId = es.getEventId();
				if(!isAnonymousEvent(eventId)){
					consolidated.add(s);
				}else{
					Stat sMapped = anonMap.get(eventId);
					if(sMapped != null){
						sMapped.setCount(sMapped.getCount() + s.getCount());
						if(s.getDate().after(sMapped.getDate()))
							sMapped.setDate(s.getDate());
						anonMap.put(eventId, sMapped);
					}else{
						s.setUserId(null);
						anonMap.put(eventId, s);
					}
				}
			}
		}

		for(Stat s : anonMap.values()){
			consolidated.add(s);
		}

		report.setReportData(consolidated);
		return report;
	}

	private boolean isAnonymousEvent(String eventId) {
		for(ToolInfo ti : eventRegistryService.getEventRegistry()){
			for(EventInfo ei : ti.getEvents()){
				if(ei.getEventId().equals(eventId)){
					return ei.isAnonymous();
				}
			}
		}
		return false;
	}

	private StringBuilder appendQuoted(StringBuilder sb, String toQuote) {
		if((toQuote.indexOf(',') >= 0) || (toQuote.indexOf('"') >= 0)){
			String out = toQuote.replaceAll("\"", "\"\"");
			if(log.isDebugEnabled()) log.debug("Turning '" + toQuote + "' to '" + out + "'");
			sb.append("\"").append(out).append("\"");
		}else{
			sb.append(toQuote);
		}
		return sb;
	}
	
	private String getUserDisplayId(String userId) {
		String userEid = null;		
		if (userId != null) {
			if(("-").equals(userId) || EventTrackingService.UNKNOWN_USER.equals(userId)) {
				userEid = "-";
			}else{
				try{
					userEid = userDirectoryService.getUser(userId).getDisplayId();
				}catch(UserNotDefinedException e1){
					userEid = userId;
				}
			}
		}else{
			userEid = resourceLoader.getString("user_unknown");
		}
		return userEid;
	}
	
	private String getUserDisplayName(String userId) {
		String userName = null;
		if (userId != null) {
			if(("-").equals(userId)) {
				userName = resourceLoader.getString("user_anonymous");
			}else if(EventTrackingService.UNKNOWN_USER.equals(userId)) {
				userName = resourceLoader.getString("user_anonymous_access");
			}else{
				userName = statsManager.getUserNameForDisplay(userId);
			}
		}else{
			userName = resourceLoader.getString("user_unknown");
		}
		return userName;
	}
	
	public String getSiteGroupTitle(String groupId) {
		try{
			Placement placement = toolManager.getCurrentPlacement();
			Site site = siteService.getSite(placement.getContext());
			return site.getGroup(groupId).getTitle();
		}catch(IdUnusedException e){
			log.warn("ReportManager: unable to get group title with id: " + groupId);
		}
		return null;
	}
	
	private static class ReportProcessedParams {
		public String			siteId;
		public List<String>		events;
		public List<String>		anonymousEvents;
		public List<String>		resourceIds;
		public String			resourceAction;
		public Date				iDate;
		public Date				fDate;
		public List<String>		userIds;
		public List<String> 	totalsBy;
		public boolean			inverseUserSelection;
		
		public PagingPosition	page;
		public String			sortBy;
		public boolean			sortAscending;
		public int				maxResults;
	}

	
	class ReportFormattedParamsImpl implements ReportFormattedParams {
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportSite(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportSite(Report report) {
			String site = report.getReportDefinition().getReportParams().getSiteId();
			if(site != null) {
				return siteService.getSiteDisplay(site);
			}else{
				return resourceLoader.getString("report_reportsite_all");
			}
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportTitle(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportTitle(Report report) {
			String title = report.getReportDefinition().getTitle(); 
			if(title != null && title.length() != 0) {
				if(isStringLocalized(title)) {
					return resourceLoader.getString(report.getReportDefinition().getTitleBundleKey());
				}else{
					return title;
				}
			}else{
				return null;
			}	
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportDescription(org.sakaiproject.sitestats.api.report.ReportDef)
		 */
		public String getReportDescription(Report report) {
			String description = report.getReportDefinition().getDescription(); 
			if(description != null && description.length() != 0) {
				if(isStringLocalized(description)) {
					return resourceLoader.getString(report.getReportDefinition().getDescriptionBundleKey());
				}else{
					return description;
				}
			}else{
				return null;
			}			
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#isStringLocalized(java.lang.String)
		 */
		public boolean isStringLocalized(String string) {
			if(string.startsWith("${") && string.endsWith("}")) {
				return true;
			}else{
				return false;
			}
		}

		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportGenerationDate(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportGenerationDate(Report report) {
			if(report.getReportGenerationDate() == null) {
				report.setReportGenerationDate(new Date());
			}
			Instant time = report.getReportGenerationDate().toInstant();
			return userTimeService.shortLocalizedTimestamp(time, resourceLoader.getLocale());
		}

		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportActivityBasedOn(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportActivityBasedOn(Report report) {
			if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_VISITS)
				|| report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_VISITS_TOTALS))
				return resourceLoader.getString("report_what_visits");
			else if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_PRESENCES))
				return resourceLoader.getString("report_what_presences");
			else if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_EVENTS)
				|| report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_ACTIVITY_TOTALS)){
				StringBuilder buff = new StringBuilder();
				buff.append(resourceLoader.getString("report_what_events"));
				String eventSelType = report.getReportDefinition().getReportParams().getWhatEventSelType();
				if(eventSelType != null) {
					if(eventSelType.equals(ReportManager.WHAT_EVENTS_BYTOOL)){
						buff.append(" (");
						buff.append(resourceLoader.getString("report_what_events_bytool"));
						buff.append(")");
					}else{
						buff.append(" (");
						buff.append(resourceLoader.getString("report_what_events_byevent"));
						buff.append(")");
					}
				}
				return buff.toString();
			}else if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_RESOURCES)) { 
				return resourceLoader.getString("report_what_resources");
			}else{
				return resourceLoader.getString("report_what_events");
			}			
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportActivitySelectionTitle(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportActivitySelectionTitle(Report report) {
			if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_VISITS))
				return resourceLoader.getString("report_what_visits");
			else if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_PRESENCES))
				return resourceLoader.getString("report_what_presences");
			else if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_EVENTS)){
				if(report.getReportDefinition().getReportParams().getWhatEventSelType().equals(ReportManager.WHAT_EVENTS_BYTOOL))
					return resourceLoader.getString("reportres_summ_act_tools_selected");
				else 
					return resourceLoader.getString("reportres_summ_act_events_selected");
			}else
				return resourceLoader.getString("reportres_summ_act_rsrc_selected");
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportActivitySelection(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportActivitySelection(Report report) {
			if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_VISITS)
					|| report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_PRESENCES)){
				// visits
				return null;
			}else if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_EVENTS)){
				if(report.getReportDefinition().getReportParams().getWhatEventSelType().equals(ReportManager.WHAT_EVENTS_BYTOOL)){
					// tools
					List<String> list = report.getReportDefinition().getReportParams().getWhatToolIds();
					int listSize = list.size();
					StringBuilder buff = new StringBuilder();
					if(listSize > 0) {
						for(int i=0; i<listSize - 1; i++){
							String toolId = list.get(i);
							buff.append(eventRegistryService.getToolName(toolId));
							buff.append(", ");
						}
						String toolId = list.get(listSize - 1);
						buff.append(eventRegistryService.getToolName(toolId));
					}
					return buff.toString();
				}else{
					// events
					List<String> list = report.getReportDefinition().getReportParams().getWhatEventIds();
					int listSize = list.size();
					StringBuilder buff = new StringBuilder();
					if(listSize > 0) {
						for(int i=0; i<listSize - 1; i++){
							String eventId = list.get(i);
							buff.append(eventRegistryService.getEventName(eventId));
							buff.append(", ");
						}
						String eventId = list.get(listSize - 1);
						buff.append(eventRegistryService.getEventName(eventId));
					}
					return buff.toString();
				}
			}else if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_RESOURCES)){
				// resources
				List<String> list = report.getReportDefinition().getReportParams().getWhatResourceIds();
				if(report.getReportDefinition().getReportParams().getWhatResourceIds() == null
						|| report.getReportDefinition().getReportParams().getWhatResourceIds().size() == 0 )
					return null;
				if(list.contains("all"))
					return resourceLoader.getString("report_what_all");
				StringBuilder buff = new StringBuilder();
				String siteId = report.getReportDefinition().getReportParams().getSiteId();
				String resourcesCollectionId = contentHostingService.getSiteCollection(siteId);
				String dropboxCollectionId = contentHostingService.getDropboxCollection(siteId);
				String attachmentsCollectionId = resourcesCollectionId.replaceFirst(StatsManager.RESOURCES_DIR, StatsManager.ATTACHMENTS_DIR);
				for(int i=0; i<list.size(); i++){
					String resourceId = list.get(i);
					try{
						if(resourceId.endsWith("/")) {
							if(StatsManager.RESOURCES_DIR.equals(resourceId) || resourceId.equals(resourcesCollectionId)) {
								buff.append(toolManager.getTool(StatsManager.RESOURCES_TOOLID).getTitle());
							}else if(StatsManager.DROPBOX_DIR.equals(resourceId) || resourceId.equals(dropboxCollectionId)) {
								buff.append(toolManager.getTool(StatsManager.DROPBOX_TOOLID).getTitle());
							}else if(resourceId.startsWith(dropboxCollectionId)) {
								buff.append(toolManager.getTool(StatsManager.DROPBOX_TOOLID).getTitle());
								buff.append(": ");
								ContentCollection cc = contentHostingService.getCollection(resourceId);
								String ccName = cc.getProperties().getProperty(ResourceProperties.PROP_DISPLAY_NAME);	
								buff.append(ccName);
							}else if(StatsManager.ATTACHMENTS_DIR.equals(resourceId) || resourceId.equals(attachmentsCollectionId)) {
								buff.append(resourceLoader.getString("report_content_attachments"));
							}else if(resourceId.startsWith(attachmentsCollectionId)) {
								buff.append(resourceLoader.getString("report_content_attachments"));
								buff.append(": ");
								ContentCollection cc = contentHostingService.getCollection(resourceId);
								String ccName = cc.getProperties().getProperty(ResourceProperties.PROP_DISPLAY_NAME);	
								buff.append(ccName);
							}else{
								ContentCollection cc = contentHostingService.getCollection(resourceId);
								String ccName = cc.getProperties().getProperty(ResourceProperties.PROP_DISPLAY_NAME);	
								buff.append(ccName);
							}
						}else{
							ContentResource cr = contentHostingService.getResource(resourceId);
							String crName = cr.getProperties().getProperty(ResourceProperties.PROP_DISPLAY_NAME);	
							buff.append(crName);
						}
						if(list.size() > 1 && i != list.size() - 1) {
							buff.append(", ");
						}
					}catch(PermissionException e){
						log.error(e.getMessage(), e);
					}catch(IdUnusedException e){
						log.error(e.getMessage(), e);
					}catch(TypeException e){
						log.error(e.getMessage(), e);
					}
				}
				return buff.toString();
			}else {
                return null;
            }
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportResourceActionTitle(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportResourceActionTitle(Report report) {
			if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_RESOURCES)
					&& report.getReportDefinition().getReportParams().getWhatResourceAction() != null)
					return resourceLoader.getString("reportres_summ_act_rsrc_action");
			return null;
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportResourceAction(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportResourceAction(Report report) {
			if(report.getReportDefinition().getReportParams().getWhat().equals(ReportManager.WHAT_RESOURCES)
					&& report.getReportDefinition().getReportParams().getWhatResourceAction() != null){
				return resourceLoader.getString("action_" + report.getReportDefinition().getReportParams().getWhatResourceAction());
			}else
				return null;
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportTimePeriod(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportTimePeriod(Report report) {
			if(report.getReportDefinition().getReportParams().getWhen().equals(ReportManager.WHEN_ALL)){
				return resourceLoader.getString("report_when_all");
			}else{
				ReportParams rp = report.getReportDefinition().getReportParams();
				ZonedDateTime from = ZonedDateTime.ofInstant(rp.getWhenFrom().toInstant(), ZoneId.systemDefault());
				ZonedDateTime to = ZonedDateTime.ofInstant(rp.getWhenTo().toInstant(), ZoneId.systemDefault());
				String timeZoneMsg = resourceLoader.getFormattedMessage("report_server_time_zone", statsManager.getLocalSakaiName());

				return userTimeService.shortLocalizedDate(from.toLocalDate(), resourceLoader.getLocale())
						+ " - " + userTimeService.shortLocalizedDate(to.toLocalDate(), resourceLoader.getLocale()) + " " + timeZoneMsg;
			}
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportUserSelectionType(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportUserSelectionType(Report report) {
			if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_ALL))
				return resourceLoader.getString("report_who_all");
			else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_GROUPS))
				return resourceLoader.getString("report_who_group");
			else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_ROLE))
				return resourceLoader.getString("report_who_role");
			else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_CUSTOM))
				return resourceLoader.getString("report_who_custom");
			else 
				return resourceLoader.getString("report_who_not_match");
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportUserSelectionTitle(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportUserSelectionTitle(Report report) {
			if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_ALL))
				return null;
			else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_GROUPS))
				return resourceLoader.getString("reportres_summ_usr_group_selected");
			else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_ROLE))
				return resourceLoader.getString("reportres_summ_usr_role_selected");
			else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_CUSTOM))
				return resourceLoader.getString("reportres_summ_usr_users_selected");
			else 
				return null;		
		}
		
		/* (non-Javadoc)
		 * @see org.sakaiproject.sitestats.api.report.ReportFormattedParams#getReportUserSelection(org.sakaiproject.sitestats.api.report.Report)
		 */
		public String getReportUserSelection(Report report) {
			if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_GROUPS)){
				return getSiteGroupTitle(report.getReportDefinition().getReportParams().getWhoGroupId());
			}else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_ROLE)){
				return report.getReportDefinition().getReportParams().getWhoRoleId();
			}else if(report.getReportDefinition().getReportParams().getWho().equals(ReportManager.WHO_CUSTOM)){
				// users
				List<String> list = report.getReportDefinition().getReportParams().getWhoUserIds();
				StringBuilder buff = new StringBuilder();
				for(int i=0; i<list.size() - 1; i++){
					String userId = list.get(i);
					buff.append(getUserDisplayId(userId));
					buff.append(", ");
				}
				String userId = list.get(list.size() - 1);
				buff.append(getUserDisplayId(userId));
				return buff.toString();
			}else
				return null;
		}
	}
	
	private static class KeyReportDefList {
		public String siteId;
		public boolean includedPredefined;
		public boolean includeHidden;
		
		public KeyReportDefList(String siteId, boolean includedPredefined, boolean includeHidden){
			this.siteId = siteId;
			this.includedPredefined = includedPredefined;
			this.includeHidden = includeHidden;
		}

		@Override
		public boolean equals(Object o) {
			if(o instanceof KeyReportDefList) {
				KeyReportDefList u = (KeyReportDefList) o;
				return 
					(
						(siteId == null && u.siteId == null) 
						|| (siteId != null && siteId.equals(u.siteId))
					) 
					&& includedPredefined==u.includedPredefined 
					&& includeHidden==u.includeHidden;
			}
			return false;
		}

		@Override
		public int hashCode() {
			return (siteId!=null?siteId.hashCode():0) 
				+ (includedPredefined?1:0) 
				+ (includeHidden?1:0);
		}

        @Override
        public String toString() {
            return siteId+",p:"+includedPredefined+",h:"+includeHidden;
        }
    }
}
