/**
* Copyright (C) 2011-2015 The XDocReport Team <xdocreport@googlegroups.com>
*
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package fr.opensagres.struts2.views.xdocreport;
import static fr.opensagres.struts2.views.xdocreport.ActionInvocationUtils.getRequest;
import static fr.opensagres.struts2.views.xdocreport.ActionInvocationUtils.getResponse;
import static fr.opensagres.struts2.views.xdocreport.ActionInvocationUtils.getServletContext;
import static fr.opensagres.xdocreport.core.utils.StringUtils.FALSE;
import static fr.opensagres.xdocreport.core.utils.StringUtils.TRUE;
import static fr.opensagres.xdocreport.core.utils.StringUtils.asBoolean;
import static fr.opensagres.xdocreport.core.utils.StringUtils.isEmpty;
import static fr.opensagres.xdocreport.core.utils.StringUtils.isNotEmpty;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.dispatcher.StrutsResultSupport;
import com.opensymphony.xwork2.ActionInvocation;
import fr.opensagres.xdocreport.converter.ConverterTypeTo;
import fr.opensagres.xdocreport.converter.IConverter;
import fr.opensagres.xdocreport.converter.IURIResolver;
import fr.opensagres.xdocreport.converter.MimeMapping;
import fr.opensagres.xdocreport.converter.Options;
import fr.opensagres.xdocreport.converter.OptionsHelper;
import fr.opensagres.xdocreport.converter.XDocConverterException;
import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.core.logging.LogUtils;
import fr.opensagres.xdocreport.core.utils.StringUtils;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.document.web.WEBURIResolver;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;
/**
* Abstract class for manage Struts2 Result with XDocReport to generate odt, docx report by using odt, docx document and
* convert it to another format like PDF/XHTML.
*/
public abstract class AbstractXDocReportResult
extends StrutsResultSupport
{
private static final long serialVersionUID = -3844927561499091875L;
/**
* Logger for this class
*/
private static final Logger LOGGER = LogUtils.getLogger( AbstractXDocReportResult.class );
// HTTP Header constants
private static final String SAT_6_MAY_1995_12_00_00_GMT = "Sat, 6 May 1995 12:00:00 GMT";
private static final String EXPIRES = "Expires";
private static final String POST_CHECK_0_PRE_CHECK_0 = "post-check=0, pre-check=0";
private static final String NO_CACHE = "no-cache";
private static final String PRAGMA = "Pragma";
private static final String NO_STORE_NO_CACHE_MUST_REVALIDATE = "no-store, no-cache, must-revalidate";
private static final String CACHE_CONTROL_HTTP_HEADER = "Cache-Control";
// Content-Disposition HTTP response Header
private static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition";
private static final String ATTACHMENT_FILENAME = "attachment; filename=\"";
private static final String WEB_URI_RESOLVER_DATA_KEY = WEBURIResolver.class.getName();
protected static final String ACTION_KEY = "#action";
private static final Map<Class, Boolean> isXDocReportInitializerAwareClassCache = new HashMap<Class, Boolean>();
public static final String LAST_MODIFIED = AbstractXDocReportResult.class.getSimpleName() + "_LAST_MODIFIED";
private String templateEngine = TemplateEngineKind.Velocity.name();
private String converter = null;
private String[] expressions = { ACTION_KEY };
private String trackLastModified = FALSE;
private String download = TRUE;
private String fieldsAsList = null;
/**
* Set Template engine to use (according the JAR which is added in the classpath, values are Velocity|Freemarker).
* The template engine can use OGNL expression too (ex : #action.templateEngine will use the
* Action#getTemplateEngine() method).
*
* @param templateEngine
*/
public void setTemplateEngine( String templateEngine )
{
this.templateEngine = templateEngine;
}
/**
* Get Template engine to use (according the JAR which is added in the classpath, values are Velocity|Freemarker).
*
* @return
*/
public String getTemplateEngine()
{
return templateEngine;
}
/**
* Set the converter to use (according the JAR which is added in the classpath, values are static value
* PDF_ITEXT|XHTML_XWPF ). The converter can use OGNL expression too (ex : #action.converter will use the
* Action#getConverter() method). If converter is null, no conversion is done.
*
* @param converter
*/
public void setConverter( String converter )
{
this.converter = converter;
}
/**
* Returns the converter to use (according the JAR which is added in the classpath, values are
* PDF_ITEXT|XHTML_XWPF). If converter is null, no conversion is done.
*
* @return
*/
public String getConverter()
{
return converter;
}
/**
* @param expression
*/
public void setExpression( String expression )
{
this.expressions = expression.split( "," );
}
/**
* @return
*/
public String[] getExpressions()
{
return expressions;
}
/**
* Set to true if docx, odt file which is used to load must be tracked to observe the document change. If document
* change, report will be reloaded with the new file.
*
* @param trackLastModified
*/
public void setTrackLastModified( String trackLastModified )
{
this.trackLastModified = trackLastModified;
}
/**
* Returns true if docx, odt file which is used to load must be tracked to observe the document change and false
* otherwise. If document change, report will be reloaded with the new file.
*
* @return
*/
public String getTrackLastModified()
{
return trackLastModified;
}
/**
* Set to true if report must be downloaded (generate Content-Disposition:"attachment; filename... in the HTTP
* Header) and false otherwise. By default download is true.
*
* @param download
*/
public void setDownload( String download )
{
this.download = download;
}
/**
* Returns true if report must be downloaded (generate Content-Disposition:"attachment; filename... in the HTTP
* Header) and false otherwise.
*
* @return
*/
public String getDownload()
{
return download;
}
public void setFieldAsList( String fieldAsList )
{
this.fieldsAsList = fieldAsList;
}
public String[] getFieldsAsList()
{
if ( isEmpty( fieldsAsList ) )
{
return StringUtils.EMPTY_STRING_ARRAY;
}
return fieldsAsList.split( "," );
}
@Override
protected void doExecute( String finalLocation, ActionInvocation invocation )
throws Exception
{
String location = getLocation( finalLocation, invocation );
long startTime = -1;
if ( LOGGER.isLoggable( Level.FINE ) )
{
startTime = System.currentTimeMillis();
LOGGER.fine( String.format( "Start XDocReportResult for location=%s, templateEngine %s", location,
getTemplateEngine() ) );
}
try
{
// 1) Get or load Report from the registry
XDocReportRegistry registry = XDocReportRegistry.getRegistry();
IXDocReport report = getReport( registry, location, invocation );
if ( report == null )
{
throw new XDocReportException( "Cannot get XDoc Report for location=" + location );
}
// 2) Populate Java model context with Struts2 value Stack
IContext context = report.createContext();
populateContext( report, context, location, invocation );
// 3) Process or convert the report
Options options = getOptionsConverter( report, location, invocation );
if ( options == null )
{
doProcessReport( report, context, location, invocation );
}
else
{
doProcessReportWithConverter( report, context, options, location, invocation );
}
if ( LOGGER.isLoggable( Level.FINE ) )
{
startTime = System.currentTimeMillis();
LOGGER.fine( String.format( "End XDocReportResult with SUCCESS for location=%s, templateEngine %s done in %s ms",
location, getTemplateEngine(), ( System.currentTimeMillis() - startTime ) ) );
}
}
catch ( Throwable e )
{
if ( LOGGER.isLoggable( Level.FINE ) )
{
startTime = System.currentTimeMillis();
LOGGER.fine( String.format( "End XDocReportResult with SUCCESS for location=%s, templateEngine %s done in %s ms",
location, getTemplateEngine(), ( System.currentTimeMillis() - startTime ) ) );
LOGGER.throwing( getClass().getName(), "doExecute", e );
}
if ( e instanceof Exception )
{
throw (Exception) e;
}
throw new ServletException( e );
}
}
// ------------------------------ Get Report
/**
* @param registry
* @param finalLocation
* @param invocation
* @return
* @throws ServletException
* @throws IOException
*/
protected IXDocReport getReport( XDocReportRegistry registry, String location, ActionInvocation invocation )
throws ServletException, IOException
{
boolean trackLastModified = isTrackLastModified( invocation );
// 1) Test if report is already registered in the registry.
String reportId = getReportId( location );
IXDocReport report = registry.getReport( reportId );
if ( report != null && !trackLastModified )
{
return report;
}
// 2) Compute location type
LocationType locationType = LocationType.getLocationType( location );
if ( report != null )
{
// Here report was already loaded and track last modified must be
// done.
if ( locationType == LocationType.CLASSPATH )
{
// Source stream comes from classpath, returns the loaded
// report.
return report;
}
}
// 3) Get the source file
File sourceFile = getSourceFile( location, locationType, invocation );
if ( report != null )
{
if ( sourceFile == null )
{
// Here report was already loaded but source file is not
// available
// (comes from CLASSPATH), returns the loaded report.
return report;
}
// Here report was already loaded and source file is
// available, compare last modified
Long lastModified = report.getData( LAST_MODIFIED );
if ( lastModified == null )
{
return report;
}
if ( lastModified >= sourceFile.lastModified() )
{
// No modification about source file.
return report;
}
// Source file was modified after load report, remove the report
// from the registry to force the load.
registry.unregisterReport( reportId );
}
// 2) Get source stream
InputStream sourceStream = getSourceStream( location, locationType, sourceFile, invocation );
if ( sourceStream == null )
{
throw new IOException( "Stream null for location=" + location );
}
try
{
report = registry.loadReport( sourceStream, reportId, getTemplateEngine( invocation ) );
report.setFieldsMetadata( getFieldsMetadata( report, location, invocation ) );
Object action = invocation.getStack().findValue( ACTION_KEY );
XDocReportInitializerAware initializer = getXDocReportInitializerAware( action );
if ( initializer != null )
{
initializer.initialize( report );
}
report.setData( LAST_MODIFIED, System.currentTimeMillis() );
return report;
}
catch ( IOException e )
{
throw new ServletException( "Inputstream", e );
}
catch ( XDocReportException e )
{
throw new ServletException( "Inputstream", e );
}
}
protected boolean isTrackLastModified( ActionInvocation invocation )
{
String trackLastModified = getTrackLastModified();
trackLastModified = getValue( trackLastModified, invocation );
return asBoolean( trackLastModified, false );
}
protected boolean isDownload( ActionInvocation invocation )
{
String download = getDownload();
download = getValue( download, invocation );
return asBoolean( download, true );
}
protected String getLocation( String finalLocation, ActionInvocation invocation )
{
return getValue( finalLocation, invocation );
}
protected String getTemplateEngine( ActionInvocation invocation )
{
String templateEngine = getTemplateEngine();
return getValue( templateEngine, invocation );
}
protected String getValue( String value, ActionInvocation invocation )
{
if ( isEmpty( value ) )
{
return value;
}
if ( value.startsWith( "#" ) )
{
return invocation.getStack().findString( value );
}
return value;
}
protected String getReportId( String finalLocation )
{
StringBuilder reportId = new StringBuilder( StringUtils.replaceAll( finalLocation, "/", "_" ) );
reportId.append( "_" );
reportId.append( getTemplateEngine() );
return reportId.toString();
}
protected InputStream getSourceStream( final String finalLocation, LocationType locationType, File sourceFile,
ActionInvocation invocation )
throws IOException
{
switch ( locationType )
{
case CLASSPATH:
String location = locationType.getLocation( finalLocation );
return getSourceStreamFromClasspath( location );
case FILESYSTEM:
return new FileInputStream( sourceFile );
default:
return new FileInputStream( sourceFile );
}
}
protected File getSourceFile( final String finalLocation, LocationType locationType, ActionInvocation invocation )
{
String location = locationType.getLocation( finalLocation );
switch ( locationType )
{
case CLASSPATH:
return null;
case FILESYSTEM:
return new File( location );
default:
ServletContext servletContext = getServletContext( invocation );
return new File( servletContext.getRealPath( location ) );
}
}
protected InputStream getSourceStreamFromClasspath( String path )
{
ClassLoader cl = this.getClass().getClassLoader();
if ( cl == null )
{
// no class loader specified -> use thread context class loader
cl = Thread.currentThread().getContextClassLoader();
}
return cl.getResourceAsStream( path );
}
protected FieldsMetadata getFieldsMetadata( IXDocReport report, String location, ActionInvocation invocation )
{
String[] fieldsAsList = getFieldsAsList();
if ( fieldsAsList != null && fieldsAsList.length > 0 )
{
FieldsMetadata fieldsMetadata = new FieldsMetadata();
for ( int i = 0; i < fieldsAsList.length; i++ )
{
fieldsMetadata.addFieldAsList( getValue( fieldsAsList[i], invocation ) );
}
return fieldsMetadata;
}
return null;
}
// --------- Process reportoid
/**
* @param report
* @param context
* @param options
* @param finalLocation
* @param invocation
* @throws IOException
* @throws XDocReportException
*/
protected void doProcessReport( IXDocReport report, IContext context, String finalLocation,
ActionInvocation invocation )
throws XDocReportException, IOException
{
HttpServletResponse response = getResponse( invocation );
// 2) Prepare HTTP response content type
prepareHTTPResponse( report.getId(), finalLocation, report.getMimeMapping(), invocation,
getRequest( invocation ), response );
// 2) Generate report
report.process( context, response.getOutputStream() );
}
// --------- Converter
/**
* @param report
* @param context
* @param finalLocation
* @param invocation
* @throws IOException
* @throws XDocReportException
* @throws XDocConverterException
*/
protected void doProcessReportWithConverter( IXDocReport report, IContext context, Options options,
String finalLocation, ActionInvocation invocation )
throws XDocConverterException, XDocReportException, IOException
{
HttpServletResponse response = getResponse( invocation );
// 2) Get converter
IConverter converter = report.getConverter( options );
// 3) Prepare HTTP response content type
prepareHTTPResponse( report.getId(), finalLocation, converter.getMimeMapping(), invocation,
getRequest( invocation ), response );
// 4) Generate report with conversion
report.convert( context, options, response.getOutputStream() );
}
/**
* @param report
* @param request
* @return
*/
protected Options getOptionsConverter( IXDocReport report, String finalLocation, ActionInvocation invocation )
{
final String converterId = getConverter( invocation );
if ( isEmpty( converterId ) )
{
return null;
}
Options options = null;
int index = converterId.lastIndexOf( '_' );
if ( index != -1 )
{
String to = converterId.substring( 0, index );
String via = converterId.substring( index + 1, converterId.length() );
options = Options.getTo( to ).via( via );
}
else
{
options = Options.getTo( converterId );
}
prepareOptions( options, report, converterId, finalLocation, invocation );
return options;
}
protected String getConverter( ActionInvocation invocation )
{
String converter = getConverter();
return getValue( converter, invocation );
}
protected void prepareOptions( Options options, IXDocReport report, String converterId, String finalLocation,
ActionInvocation invocation )
{
if ( ConverterTypeTo.FO.name().equals( options.getTo() )
|| ConverterTypeTo.XHTML.name().equals( options.getTo() ) )
{
OptionsHelper.setURIResolver( options,
createWEBURIResolver( report, converterId, finalLocation, invocation ) );
}
}
protected IURIResolver createWEBURIResolver( IXDocReport report, String converterId, String finalLocation,
ActionInvocation invocation )
{
WEBURIResolver resolver = report.getData( WEB_URI_RESOLVER_DATA_KEY );
if ( resolver == null )
{
resolver = new WEBURIResolver( report.getId(), getRequest( invocation ) );
report.setData( WEB_URI_RESOLVER_DATA_KEY, resolver );
}
return resolver;
}
protected void prepareHTTPResponse( String reportId, String finalLocation, MimeMapping mimeMapping,
ActionInvocation invocation, HttpServletRequest request,
HttpServletResponse response )
{
if ( mimeMapping != null )
{
response.setContentType( mimeMapping.getMimeType() );
}
// Check if Content-Disposition must be generated?
if ( isGenerateContentDisposition( reportId, mimeMapping, invocation ) )
{
String sourceFileName = finalLocation;
int index = finalLocation.lastIndexOf( '/' );
if ( index != -1 )
{
sourceFileName = finalLocation.substring( index + 1, finalLocation.length() );
}
String contentDisposition = getContentDisposition( sourceFileName, mimeMapping, request );
if ( isNotEmpty( contentDisposition ) )
{
response.setHeader( CONTENT_DISPOSITION_HEADER, contentDisposition.toString() );
}
}
// Disable HTTP response cache
if ( isDisableHTTPResponseCache() )
{
disableHTTPResponseCache( response );
}
}
protected boolean isGenerateContentDisposition( String reportId, MimeMapping mimeMapping,
ActionInvocation invocation )
{
return isDownload( invocation );
}
protected void prepareHTTPResponse( String reportId, String entryName, ActionInvocation invocation,
HttpServletRequest request, HttpServletResponse response )
{
// Check if Content-Disposition must be generated?
if ( isGenerateContentDisposition( reportId, null, invocation ) )
{
String contentDisposition = getContentDisposition( entryName );
if ( isNotEmpty( contentDisposition ) )
{
response.setHeader( CONTENT_DISPOSITION_HEADER, contentDisposition.toString() );
}
}
// Disable HTTP response cache
if ( isDisableHTTPResponseCache() )
{
disableHTTPResponseCache( response );
}
}
protected boolean isDisableHTTPResponseCache()
{
return true;
}
protected String getContentDisposition( String sourceFileName, MimeMapping mimeMapping, HttpServletRequest request )
{
if ( mimeMapping != null )
{
String fileName = mimeMapping.formatFileName( sourceFileName );
return getContentDisposition( fileName );
}
return null;
}
protected String getContentDisposition( String fileName )
{
StringBuilder contentDisposition = new StringBuilder( ATTACHMENT_FILENAME );
contentDisposition.append( fileName );
contentDisposition.append( "\"" );
return contentDisposition.toString();
}
/**
* Disable cache HTTP hearder.
*
* @param response
*/
protected void disableHTTPResponseCache( HttpServletResponse response )
{
// see article http://onjava.com/pub/a/onjava/excerpt/jebp_3/index2.html
// Set to expire far in the past.
response.setHeader( EXPIRES, SAT_6_MAY_1995_12_00_00_GMT );
// Set standard HTTP/1.1 no-cache headers.
response.setHeader( CACHE_CONTROL_HTTP_HEADER, NO_STORE_NO_CACHE_MUST_REVALIDATE );
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader( CACHE_CONTROL_HTTP_HEADER, POST_CHECK_0_PRE_CHECK_0 );
// Set standard HTTP/1.0 no-cache header.
response.setHeader( PRAGMA, NO_CACHE );
}
protected XDocReportInitializerAware getXDocReportInitializerAware( Object action )
{
if ( action == null )
{
return null;
}
Boolean result = isXDocReportInitializerAwareClassCache.get( action.getClass() );
if ( result == null )
{
result = ( action instanceof XDocReportInitializerAware );
isXDocReportInitializerAwareClassCache.put( action.getClass(), result );
}
if ( result )
{
return ( (XDocReportInitializerAware) action );
}
return null;
}
/**
* @param report
* @param finalLocation
* @param invocation
* @return
* @throws XDocReportException
*/
protected abstract void populateContext( IXDocReport report, IContext context, String finalLocation,
ActionInvocation invocation )
throws Exception;
}