/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
// Package
package gov.va.med.fw.report.jasperreport;

// Java classes
import java.io.ByteArrayOutputStream;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.fill.AsynchronousFillHandle;
import net.sf.jasperreports.engine.fill.AsynchronousFilllListener;

import org.apache.commons.lang.Validate;

import gov.va.med.fw.report.AbstractReportExporter;
import gov.va.med.fw.report.ReportConfiguration;
import gov.va.med.fw.report.ReportException;
import gov.va.med.fw.report.ReportTemplate;
import gov.va.med.fw.report.data.ReportData;
import gov.va.med.fw.report.jasperreport.data.JasperReportData;
import gov.va.med.fw.security.LoginManager;
import gov.va.med.fw.security.SecurityContextHelper;
import gov.va.med.fw.security.UserPrincipal;

/**
 * Provides functionalities to fill a report and to export a report to a stream 
 * synchronously and asynchronously.  To export and to fill a report asynchronously,
 * configure a bean in spring context with a asynchronous property set to true.  A 
 * default behaviour is to fill a report synchronously.
 *
 * Project: Framework</br>
 * Created on: 2:27:15 PM </br>
 *
 * @author DNS   LEV
 */
public abstract class AbstractJasperReportExporter extends AbstractReportExporter {

	/**
	 * An instance of serialVersionUID
	 */
	private static final long serialVersionUID = -856131465569866311L;

	/**
	 * A flag to indicate whether or not to fill and to export a report asynchronously
	 */
	private boolean asynchronous = false;

	/**
	 * An instance of loginManager
	 */
	private LoginManager loginManager = null;
	
	/**
	 * A default constructor
	 */
	protected AbstractJasperReportExporter() {
		super();
	}

	/**
	 * @see gov.va.med.fw.service.AbstractComponent#afterPropertiesSet()
	 */
	public void afterPropertiesSet() throws Exception {
		super.afterPropertiesSet();
      if( this.asynchronous ) {
         Validate.notNull( this.loginManager, "A login manager is missing" );
      }
	}

	/** Sets a login manager to log in to ESR to
	 * allow a security context to be present in a thread local
	 * @param loginManager
	 */
	public void setLoginManager(LoginManager loginManager) {
		this.loginManager = loginManager;
	}

	/**
	 * @return Returns the asynchronous.
	 */
	public boolean isAsynchronous() {
		return asynchronous;
	}

	/**
	 * @param asynchronous The asynchronous to set.
	 */
	public void setAsynchronous(boolean asynchronous) {
		this.asynchronous = asynchronous;
	}

	/**
	 * @see gov.va.med.fw.report.AbstractReportExporter#exportReport(gov.va.med.fw.report.ReportConfiguration, gov.va.med.fw.report.data.ReportData, gov.va.med.fw.report.ReportTemplate)
	 */
	public void exportReport(ReportConfiguration config, ReportData data, ReportTemplate template) throws ReportException {
		if( !this.isAsynchronous() ) {
			super.exportReport(config, data, template);
		}
		else {
			try {
				AsynchronousFillHandle handle = AsynchronousFillHandle.createHandle( getJasperReport( config, template ),
																									  config.getResourceMapping(),
																									  getJasperDataSource( data ) );
				ExportHandler handler = new ExportHandler( config, this );
				handle.addListener( handler );
				handle.startFill();
			}
			catch( JRException e ) {
				throw new ReportException( "Failed to populate a report asynchronously", e );
			}
		}
	}

	/** Fills a report with data
	 * 
	 * @param config
	 * @param data
	 * @param template
	 * @return
	 * @throws ReportException
	 */
	protected Object fill( ReportConfiguration config, ReportData data, ReportTemplate template ) throws ReportException {
		
		Validate.notNull( template, "A report template must not be NULL " );
		
		JasperPrint report = null;
		
		try {
			// Fill a jasper report template with data to create a ready-to-print jasper report object
			report = JasperFillManager.fillReport( this.getJasperReport( config, template ), 
																		 config.getResourceMapping(), 
																		 this.getJasperDataSource( data ) );
		}
		catch( JRException e ) {
			throw new ReportException( "Failed to populate a report ", e );
		}
		
		return report;
	}
	
	/** Exports a report to a stream
	 * 
	 * @param report A generated report
	 * @return A stream of generate report 
	 * @throws ReportException In case or errors getting an output stream of a generated report
	 */
	protected ByteArrayOutputStream export( Object report ) throws ReportException {
		
		if( !(report instanceof JasperPrint) ) {
			throw new ReportException( "Invalid report type to export: " + report );
		}
		
		return this.exportJasperReport( null, (JasperPrint)report );
	}
	
	protected JasperReport getJasperReport( ReportConfiguration config, ReportTemplate template ) throws ReportException {
		
		// Get a compiled jasper report template
		Object tpl = template.getCompiledTemplate( config );
		JasperReport jasperReport = tpl instanceof JasperReport ? (JasperReport)tpl : null;
		
		if( jasperReport == null ) {
			throw new ReportException( "Invalid type or missing report template ");
		}
		return jasperReport;
	}

	protected JRDataSource getJasperDataSource( ReportData data ) throws ReportException {
		
		// Get a jasper report data
		JasperReportData reportData = data instanceof JasperReportData ? (JasperReportData)data : null; 
		JRDataSource jasperData = reportData != null ? reportData.getJasperDataSource() : null;
		
		if( jasperData == null ) {
			throw new ReportException( "Invalid type of report data");
		}
		return jasperData;
	}

	/** Performs the actual task to export a JasperPrint (read-to-print) report to an output stream
	 * 
	 * @param print A report to be printed
	 * @return An output stream of a generated report 
	 * @throws ReportException In case or errors getting an output stream of a generated report
	 */
	protected abstract ByteArrayOutputStream exportJasperReport( ReportConfiguration config, JasperPrint print ) throws ReportException;

	/**
	 * Calls back to an AbstractExporter to export a generated report
	 * on a different thread from a caller thread.  This handler is used
	 * when an "asynchronous" flag is set to true in a derived class of 
	 * an AbstractJasperReportExporter bean definition, for example, a
	 * PDFExporter bean
	 * 
	 * @author DNS   LEV
	 */
	protected class ExportHandler implements AsynchronousFilllListener {

		private ReportConfiguration config = null;
		private AbstractJasperReportExporter exporter = null;
		
		protected ExportHandler( ReportConfiguration config, AbstractJasperReportExporter exporter ) {
			super();
			this.config = config;
			this.exporter = exporter;
		}

		public void reportFinished(JasperPrint jasperPrint) {
			// An asynch component should be configured as prototype to prevent
			// a report configuration from being altered during processing
			try {
				// SecurityContextHelper uses ThreadLocal to look for a context
				// to check if a user is currently logged in.  Because this method is called
				// in a different thread from a caller, we need to make sure that we properly
				// log in with the same user as a caller
				UserPrincipal user = this.config.getReportUser();
				if( user != null ) {
					SecurityContextHelper.initSecurityContextOnThread(this.exporter.loginManager, user.getUserCredentials());
				}
				this.exporter.persist( this.config, this.exporter.exportJasperReport( config, jasperPrint ) );
			}
			catch( Exception e ) {
				this.reportFillError( e );
			}
		}

		public void reportCancelled() {
			// A default empty implementation to allow a derived class to handle
			if( logger.isInfoEnabled() ) {
				logger.info( "A report generation is canceled");
				logger.info( "A report configuration: " + this.config );
			}
		}

		public void reportFillError(Throwable t) {
			// Simply log the exception here.  Derived classes should handle differently
			if( logger.isInfoEnabled() ) {
				logger.info( "Failed to fill / to export a report due to: ", t );
			}
		}
	}
}