/*
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://www.openbluedragon.org/
*/
package com.naryx.tagfusion.cfm.document;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.aw20.io.StreamUtil;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.tidy.Tidy;
import org.xhtmlrenderer.pdf.DefaultPDFCreationListener;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.pdf.PDFEncryption;
import org.xml.sax.InputSource;
import com.lowagie.text.DocumentException;
import com.lowagie.text.FontFactory;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfName;
import com.lowagie.text.pdf.PdfString;
import com.lowagie.text.pdf.PdfWriter;
import com.nary.io.FileUtils;
import com.nary.io.multiOutputStream;
import com.naryx.tagfusion.cfm.engine.cfBinaryData;
import com.naryx.tagfusion.cfm.engine.cfData;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfSession;
import com.naryx.tagfusion.cfm.engine.cfmBadFileException;
import com.naryx.tagfusion.cfm.engine.cfmRunTimeException;
import com.naryx.tagfusion.cfm.engine.dataNotSupportedException;
import com.naryx.tagfusion.cfm.tag.cfOptionalBodyTag;
import com.naryx.tagfusion.cfm.tag.cfTag;
import com.naryx.tagfusion.cfm.tag.cfTagReturnType;
import com.naryx.tagfusion.cfm.tag.tagLocator;
import com.naryx.tagfusion.cfm.tag.tagReader;
import com.naryx.tagfusion.cfm.xml.cfXmlData;
import com.naryx.tagfusion.cfm.xml.parse.NoValidationResolver;
import com.naryx.tagfusion.xmlConfig.xmlCFML;
public class cfDOCUMENT extends cfTag implements cfOptionalBodyTag, Serializable {
private static final long serialVersionUID = 1;
private static final String TAG_NAME = "CFDOCUMENT";
private String endMarker = null;
public static final String CFDOCUMENT_KEY = "CFDOCUMENT";
private static String[] defaultWindowsFontDirs = { "C:\\Windows\\Fonts", "C:\\WINNT\\Fonts" };
private static String[] defaultOtherFontDirs = { "/usr/X/lib/X11/fonts/TrueType", "/usr/openwin/lib/X11/fonts/TrueType",
"/usr/share/fonts/default/TrueType", "/usr/X11R6/lib/X11/fonts/ttf",
"/usr/X11R6/lib/X11/fonts/truetype", "/usr/X11R6/lib/X11/fonts/TTF" };
private static String [] defaultFontDirs;
public static void init( xmlCFML configFile ) {
String fontDirs = cfEngine.getConfig().getString( "server.fonts.dirs", "" );
if ( fontDirs.length() == 0 ) { // no fonts configured, set defaults
StringBuilder defaultFontDirsList = new StringBuilder();
if ( cfEngine.WINDOWS ) {
for ( int i = 0; i < defaultWindowsFontDirs.length; i++ ) {
if ( FileUtils.exists( defaultWindowsFontDirs[ i ] ) ) {
if ( defaultFontDirsList.length() > 0 ) { // not the first
defaultFontDirsList.append( ',' );
}
defaultFontDirsList.append( defaultWindowsFontDirs[ i ] );
}
}
} else {
for ( int i = 0; i < defaultOtherFontDirs.length; i++ ) {
if ( FileUtils.exists( defaultOtherFontDirs[ i ] ) ) {
if ( defaultFontDirsList.length() > 0 ) { // not the first
defaultFontDirsList.append( ',' );
}
defaultFontDirsList.append( defaultOtherFontDirs[ i ] );
}
}
}
if ( defaultFontDirsList.length() > 0 ) {
cfEngine.getConfig().setData( "server.fonts.dirs", defaultFontDirsList.toString() );
}
fontDirs = defaultFontDirsList.toString();
}
defaultFontDirs = fontDirs.split(",");
for ( int i = 0; i < defaultFontDirs.length; i++ ){
FontFactory.registerDirectory( defaultFontDirs[i].toString() );
}
}
public boolean doesTagHaveEmbeddedPoundSigns(){
return false;
}
public String getEndMarker(){
return endMarker;
}
public void setEndTag() {
endMarker = null;
}
public void lookAheadForEndTag(tagReader inFile) {
endMarker = (new tagLocator(TAG_NAME, inFile)).findEndMarker();
}
/*
TODO: these attributes are still to be supported
fontEmbed = "yes|no|selective" // use ITextResolver
bookmark = "yes|no"
scale = "percentage less than 100"
localUrl = "yes|no"
*/
protected void defaultParameters( String _tag ) throws cfmBadFileException {
defaultAttribute( "ENCRYPTION", "none" );
defaultAttribute( "FORMAT", "PDF" );
defaultAttribute( "FONTEMBED", "true" );
defaultAttribute( "MIMETYPE", "text/html" );
defaultAttribute( "ORIENTATION", "PORTRAIT" );
defaultAttribute( "OVERWRITE", "false" );
defaultAttribute( "PAGETYPE", "letter" );
defaultAttribute( "UNIT", "IN" );
defaultAttribute( "USERAGENT", "OpenBD" );
defaultAttribute( "BACKGROUNDVISIBLE", "true" );
parseTagHeader( _tag );
}
public cfTagReturnType render( cfSession _Session ) throws cfmRunTimeException {
if ( !getDynamic( _Session, "FORMAT" ).getString().equalsIgnoreCase( "PDF" ) ){
throw newRunTimeException( "Invalid FORMAT value. Only \"PDF\" is supported." );
}
if ( containsAttribute( "SRC" ) && containsAttribute( "SRCFILE" ) ){
throw newRunTimeException( "Invalid attribute combination. Either the SRC or SRCFILE attribute must be specified but not both" );
}
ITextRenderer renderer = new ITextRenderer();
CreationListener listener = new CreationListener(getDynamic(_Session, "AUTHOR"),getDynamic(_Session, "TITLE"),getDynamic(_Session, "SUBJECT"),getDynamic(_Session, "KEYWORDS"));
renderer.setListener(listener);
resolveFonts( _Session, renderer );
if ( _Session.getDataBin( CFDOCUMENT_KEY ) != null ){
throw newRunTimeException( "CFDOCUMENT cannot be embedded within another CFDOCUMENT tag" );
}
_Session.setDataBin( CFDOCUMENT_KEY, new DocumentContainer() );
String renderedBody = renderToString( _Session ).getOutput();
try{
DocumentContainer container = (DocumentContainer) _Session.getDataBin( CFDOCUMENT_KEY );
List<DocumentSection> sections = container.getSections();
if ( sections.size() == 0 ){
// if no sections are specified then construct one from this tag
DocumentSection section = new DocumentSection();
section.setHeader( container.getMainHeader(), container.getMainHeaderAlign() );
section.setFooter( container.getMainFooter(), container.getMainFooterAlign() );
if ( renderedBody.length() == 0 && !( containsAttribute( "SRC" ) || containsAttribute( "SRCFILE" ) ) ){
throw newRunTimeException( "Cannot create a PDF from an empty document!" );
}
String src = containsAttribute( "SRC" ) ? getDynamic( _Session, "SRC" ).getString() : null;
String srcFile = containsAttribute( "SRCFILE" ) ? getDynamic( _Session, "SRCFILE" ).getString() : null;
section.setSources( src, srcFile, renderedBody );
appendSectionAttributes( _Session, section );
sections.add( section );
}
DocumentSettings settings = getDocumentSettings( _Session, container );
// If there is more than 1 section and page counters are used that need special
// processing then we need to do an initial conversion of the HTML to PDF to
// determine how many pages are created per section and how many pages are created total.
if ((sections.size() > 1) && (container.usesTotalPageCounters()))
preparePageCounters( _Session, renderer, sections, settings );
preparePDF( _Session, renderer, sections, settings );
return cfTagReturnType.NORMAL;
}finally{
_Session.deleteDataBin( CFDOCUMENT_KEY );
}
}
private void resolveFonts( cfSession _Session, ITextRenderer _renderer ) throws dataNotSupportedException, cfmRunTimeException{
ITextFontResolver resolver = _renderer.getFontResolver();
boolean embed = getDynamic( _Session, "FONTEMBED" ).getBoolean();
for ( int i = 0; i < defaultFontDirs.length; i++ ){
File nextFontDir = new File( defaultFontDirs[i] );
File[] fontFiles = nextFontDir.listFiles( new FilenameFilter() {
public boolean accept( File _dir, String _name ){
String name = _name.toLowerCase();
return name.endsWith( ".otf" ) || name.endsWith( ".ttf" );
}
});
if ( fontFiles != null ){
for ( int f = 0; f < fontFiles.length; f++ ){
try{
resolver.addFont( fontFiles[f].getAbsolutePath(), BaseFont.IDENTITY_H,
embed );
}catch( Exception ignored ){} // ignore fonts that can't be added
}
}
}
}
/*
* preparePageCounters
*
* This method is expensive because it requires us to convert the HTML to PDF to
* determine the total page counters. This method should only be called if there is more
* than one section and one of the following page counter variables is being used:
*
* 1. TotalPageCount
* 2. TotalSectionPageCount
*/
private void preparePageCounters( cfSession _Session, ITextRenderer _renderer, List<DocumentSection> _sections, DocumentSettings _settings ) throws cfmRunTimeException{
OutputStream pdfOut = null;
try{
pdfOut = new NullOutputStream();
DocumentSection nextSection = _sections.get( 0 );
if (nextSection.pageCounterConflict())
throw newRunTimeException("OpenBD doesn't support currentpagenumber and currentsectionpagenumber in same section.");
String renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() );
_renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) );
_renderer.layout();
_renderer.createPDF( pdfOut, false );
int currentPageNumber = _renderer.getWriter().getCurrentPageNumber();
nextSection.setTotalSectionPageCount(currentPageNumber);
int totalPageCount = currentPageNumber;
for ( int i = 1; i < _sections.size(); i++ ){
nextSection = _sections.get( i );
if (nextSection.pageCounterConflict())
throw newRunTimeException("OpenBD doesn't support currentpagenumber and currentsectionpagenumber in same section.");
renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() );
_renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) );
_renderer.layout();
_renderer.writeNextDocument( _renderer.getWriter().getCurrentPageNumber()+1 );
currentPageNumber = _renderer.getWriter().getCurrentPageNumber();
nextSection.setTotalSectionPageCount(currentPageNumber-totalPageCount);
totalPageCount = currentPageNumber;
}
for ( int i = 0; i < _sections.size(); i++ ){
nextSection = _sections.get( i );
nextSection.setTotalPageCount(totalPageCount);
}
} catch (DocumentException e) {
throw newRunTimeException( "Failed to create PDF due to DocumentException: " + e.getMessage() );
}finally{
if ( pdfOut != null )try{ pdfOut.close(); }catch( IOException ignored ){}
}
}
private void preparePDF( cfSession _Session, ITextRenderer _renderer, List<DocumentSection> _sections, DocumentSettings _settings ) throws cfmRunTimeException{
OutputStream pdfOut = null;
try{
ByteArrayOutputStream bos = null;
if ( containsAttribute( "FILENAME" ) ){
File pdfFile = new File( getDynamic( _Session, "FILENAME" ).getString() );
if ( pdfFile.exists() && !getDynamic( _Session, "OVERWRITE" ).getBoolean() ){
throw newRunTimeException( "PDF file already exists and overwrite is disabled." );
}
pdfOut = cfEngine.thisPlatform.getFileIO().getFileOutputStream(pdfFile);
if ( containsAttribute( "NAME" ) ){
bos = new ByteArrayOutputStream();
pdfOut = new multiOutputStream( pdfOut, bos );
}
}else if ( containsAttribute( "NAME" ) ){
pdfOut = new ByteArrayOutputStream();
}else{
_Session.resetBuffer();
// The SAVEASNAME attribute as been tested with the following:
//
// IE8 - Page/Save As : FAILS
// IE8 - PDF plugin save image : FAILS
// Firefox 3.5.8 - File/Save Page As : WORKS
// Firefox 3.5.8 - PDF plugin save image : FAILS
//
String saveAsName;
if ( containsAttribute( "SAVEASNAME" ) ){
saveAsName = getDynamic( _Session, "SAVEASNAME" ).toString();
} else {
// Extract the filename from the path and use it as the save as name
saveAsName = _Session.REQ.getServletPath();
int slash = saveAsName.lastIndexOf('/');
if ( slash != -1 )
saveAsName = saveAsName.substring(slash+1);
int dot = saveAsName.lastIndexOf('.');
if ( dot != -1 )
saveAsName = saveAsName.substring(0,dot) + ".pdf";
}
pdfOut = new SessionOutputStream( _Session, saveAsName );
}
// handle encryption/password if attributes are set
if ( containsAttribute("OWNERPASSWORD") || containsAttribute("USERPASSWORD") || containsAttribute("PERMISSIONS") ||
!getDynamic( _Session, "ENCRYPTION" ).getString().equalsIgnoreCase("none")){
PDFEncryption mEnc = new PDFEncryption();
setPermissions( _Session, mEnc );
_renderer.setPDFEncryption( mEnc );
}
DocumentSection nextSection = _sections.get( 0 );
String renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() );
_renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) );
_renderer.layout();
_renderer.createPDF( pdfOut, false );
for ( int i = 1; i < _sections.size(); i++ ){
nextSection = _sections.get( i );
renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() );
_renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) );
_renderer.layout();
if ( nextSection.usesCurrentPageNumber() ) {
// uses currentpagenumber so start page numbering with current page number + 1
_renderer.writeNextDocument( _renderer.getWriter().getCurrentPageNumber() + 1 );
} else {
// uses currentsectionpagenumber so start page numbering with 1
_renderer.writeNextDocument( 1 );
}
}
_renderer.finishPDF();
if ( pdfOut instanceof SessionOutputStream ){
if( ( (SessionOutputStream) pdfOut).getException() != null ){
throw ( (SessionOutputStream) pdfOut).getException();
}
_Session.pageFlush();
_Session.abortPageProcessing();
}
// Add the data to our session
if ( containsAttribute( "NAME" ) )
{
if ( bos != null )
_Session.setData(getDynamic(_Session, "NAME").toString(), new cfBinaryData(bos.toByteArray()));
else
_Session.setData(getDynamic(_Session, "NAME").toString(), new cfBinaryData(((ByteArrayOutputStream)pdfOut).toByteArray()));
}
} catch (DocumentException e) {
throw newRunTimeException( "Failed to create PDF due to DocumentException: " + e.getMessage() );
} catch ( IOException e ) {
throw newRunTimeException( "Error writing PDF to file. Check the file exists and can be written to." );
}finally{
if ( pdfOut != null )try{ pdfOut.close(); }catch( IOException ignored ){}
}
}
private String getRenderedBody( cfSession _Session, DocumentSection _section, DocumentSettings _settings, int _numSections ) throws cfmRunTimeException{
String renderedBody = _section.getBody();
if ( renderedBody.length() != 0 ){
// If the section had a mimetype specified and it's text/plain or if it didn't have a mimetype
// specified and the document mimetype was set to text/plain then treat the body as plain text.
if ((( _section.getMimeType() != null ) && (_section.getMimeType().equalsIgnoreCase("text/plain"))) ||
(( _section.getMimeType() == null ) && (getDynamic(_Session,"MIMETYPE").getString().equalsIgnoreCase("text/plain"))))
renderedBody = getXHTML( "<pre>" + escapeHtmlChars(renderedBody) + "</pre>" );
else
renderedBody = getXHTML( renderedBody );
}else{
renderedBody = getXHTML( retrieveDocument( _Session, _section, _settings ) );
}
renderedBody = insertStyles( _Session, renderedBody, _section, _settings, getDynamic(_Session, "BACKGROUNDVISIBLE").getBoolean(), _numSections );
return renderedBody;
}
public Document getDocument( String _renderedBody ) throws cfmRunTimeException{
try{
DocumentBuilder builder;
InputSource is = new InputSource( new StringReader( _renderedBody ) );
Document doc;
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setValidating( false );
builder = builderFactory.newDocumentBuilder();
builder.setEntityResolver( new NoValidationResolver() );
doc = builder.parse( is );
return doc;
} catch (Exception e) {
throw newRunTimeException( "Failed to create valid xhtml document due to " + e.getClass().getName() + ": " + e.getMessage() );
}
}
private String getXHTML( String _html ){
Tidy tidy = new Tidy();
tidy.setQuiet( true );
tidy.setNumEntities( true );
tidy.setShowWarnings( false );
StringWriter result = new StringWriter();
tidy.setMakeClean( true );
tidy.setXHTML( true );
tidy.parse( new StringReader( _html ), result );
return result.toString();
}
private String retrieveDocument( cfSession _Session, DocumentSection _section, DocumentSettings _settings ) throws dataNotSupportedException, cfmRunTimeException{
String src = _section.getSrc();
if ( src != null ){
// if this is a file:// url then just handle it as a SRCFILE rather
// than pass it through HttpClient
if ( src.startsWith( "file://" ) ){
return retrieveLocalFile( _Session, src.substring( 8 ) );
}else if ( src.startsWith( "http://") || src.startsWith( "https://") ){
return retrieveHttp( _Session, src, _section, _settings );
}else{
return retrieveHttp( _Session, makeAbsoluteUrl(src, _Session), _section, _settings );
}
}else{
return retrieveLocalFile( _Session, _section.getSrcFile() );
}
}
private static String makeAbsoluteUrl(String relativeUrl, cfSession _Session)
{
boolean serverRelative = false;
if (relativeUrl.startsWith("/"))
serverRelative = true;
StringBuffer base = new StringBuffer(_Session.REQ.getScheme());
base.append("://");
base.append(_Session.REQ.getServerName());
if (_Session.REQ.getServerPort() != 80)
base.append(":" + _Session.REQ.getServerPort());
if (serverRelative)
{
base.append(relativeUrl);
}
else
{
if (_Session.REQ.getContextPath().equals(""))
base.append("/");
base.append(_Session.REQ.getContextPath());
base.append(_Session.REQ.getServletPath().substring(
0, _Session.REQ.getServletPath().lastIndexOf("/")));
base.append("/" + relativeUrl);
}
return base.toString();
}
private String retrieveLocalFile( cfSession _Session, String _filepath ) throws cfmRunTimeException{
File file = new File( _filepath );
if (!file.exists())
file = new File(_Session.getPresentDirectory(), _filepath);
FileInputStream fin = null;
try{
fin = new FileInputStream( file );
return handleDocument( _Session, fin, null );
} catch (FileNotFoundException e) {
throw newRunTimeException( "Invalid file specified. " + _filepath + " could not be found" );
} catch (IOException e) {
throw newRunTimeException( "Failed to read specified file " + _filepath + ". Check the sufficient permissions have been set to permit reading of this file." );
}finally{
if ( fin != null )try{ fin.close(); }catch( Exception ignored ){}
}
}
private String retrieveHttp( cfSession _Session, String _src, DocumentSection _section, DocumentSettings _defaultSettings ) throws dataNotSupportedException, cfmRunTimeException{
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet method = new HttpGet();
try {
method.setURI( new URI( _src ) );
if ( _section.getUserAgent() != null ){
method.setHeader( "User-Agent", _section.getUserAgent() );
}else{
method.setHeader( "User-Agent", _defaultSettings.getUserAgent() );
}
// HTTP basic authentication
if ( _section.getAuthPassword() != null ){
httpClient.getCredentialsProvider().setCredentials( AuthScope.ANY, new UsernamePasswordCredentials( _section.getAuthUser(), _section.getAuthPassword() ) );
}
// proxy support
if ( _defaultSettings.getProxyHost() != null ){
HttpHost proxy = new HttpHost( _defaultSettings.getProxyHost() , _defaultSettings.getProxyPort() );
httpClient.getParams().setParameter( ConnRoutePNames.DEFAULT_PROXY, proxy );
if ( _defaultSettings.getProxyUser() != null ){
httpClient.getCredentialsProvider().setCredentials( new AuthScope( _defaultSettings.getProxyHost() , _defaultSettings.getProxyPort() ),
new UsernamePasswordCredentials( _defaultSettings.getProxyUser(), _defaultSettings.getProxyPassword() ) );
}
}
HttpResponse response;
response = httpClient.execute( method );
if ( response.getStatusLine().getStatusCode() == 200 ){
String charset = null;
Header contentType = response.getFirstHeader( "Content-type" );
if ( contentType != null ){
String value = contentType.getValue();
int indx = value.indexOf( "charset=" );
if ( indx > 0 ){
charset = value.substring( indx+8 ).trim();
}
}
return handleDocument( _Session, response.getEntity().getContent(), charset );
}else{
throw newRunTimeException( "Failed to retrieve document from source. HTTP status code " + response.getStatusLine().getStatusCode() + " was returned" );
}
// throw newRunTimeException( "Failed to retrieve document from " + _src + " due to HttpException: " + e.getMessage() );
} catch (URISyntaxException e) {
throw newRunTimeException( "Error retrieving document via http: " + e.getMessage() );
} catch (IOException e) {
throw newRunTimeException( "Error retrieving document via http: " + e.getMessage() );
}
}
private DocumentSettings getDocumentSettings( cfSession _Session, DocumentContainer _container ) throws dataNotSupportedException, cfmRunTimeException{
DocumentSettings settings = new DocumentSettings();
// get UNIT value
String unit = getDynamic( _Session, "UNIT" ).getString().toLowerCase();
if ( !unit.equals( "in" ) && !unit.equals( "cm" ) ){
throw newRunTimeException( "Invalid UNIT value. Valid values include \"IN\" and \"CM\"." );
}
settings.setUnit( unit );
// set margins
if ( containsAttribute( "MARGINTOP" ) ){
settings.setMarginTop( getDynamic( _Session, "MARGINTOP" ).getString() );
}
if ( containsAttribute( "MARGINLEFT" ) ){
settings.setMarginLeft( getDynamic( _Session, "MARGINLEFT" ).getString() );
}
if ( containsAttribute( "MARGINRIGHT" ) ){
settings.setMarginRight( getDynamic( _Session, "MARGINRIGHT" ).getString() );
}
if ( containsAttribute( "MARGINBOTTOM" ) ){
settings.setMarginBottom( getDynamic( _Session, "MARGINBOTTOM" ).getString() );
}
// page type and size
String pageType = getDynamic( _Session, "PAGETYPE" ).getString().toLowerCase();
String pageSize = "a4";
boolean landscape = false; // default to portrait
if ( containsAttribute( "ORIENTATION" ) ){
String orientStr = getDynamic( _Session, "ORIENTATION" ).getString();
if ( orientStr.equalsIgnoreCase( "LANDSCAPE" ) ){
landscape = true;
}else if ( !orientStr.equalsIgnoreCase( "PORTRAIT" ) ){
throw newRunTimeException( "Invalid ORIENTATION value. Valid values include \"PORTRAIT\" and \"LANDSCAPE\"." );
}
}
if ( pageType.equals( "a4" ) || pageType.equals( "a5" ) || pageType.equals( "b4" ) || pageType.equals( "legal" ) || pageType.endsWith( "letter" )){
pageSize = pageType;
if ( landscape ){
pageSize += " landscape";
}
}else{
String width = null;
String height = null;
if ( pageType.equals( "b5" ) ){
width = "7in";
height = "9.88in";
}else if ( pageType.equals( "b5-jis" ) ){
width = "7.19in";
height = "10.13in";
}else if ( pageType.equals( "b4-jis" ) ){
width = "10.13in";
height = "14.31in";
}else if ( pageType.equals( "custom" ) ){
if ( !containsAttribute( "PAGEHEIGHT" ) || !containsAttribute( "PAGEWIDTH" ) ){
throw newRunTimeException( "Missing PAGEHEIGHT/PAGEWIDTH attribute(s). Both must be specified when specifying a CUSTOM page size" );
}
width = getDynamic( _Session, "PAGEWIDTH" ).getString() + unit;
height = getDynamic( _Session, "PAGEHEIGHT" ).getString() + unit;
}else{
throw newRunTimeException( "Invalid PAGETYPE value." );
}
if ( landscape ){
String tmp = height;
height = width;
width = tmp;
}
pageSize = width + " " + height;
}
settings.setPageSize( pageSize );
// proxy details
if ( containsAttribute( "PROXYHOST" ) ){
String proxyHost = getDynamic( _Session, "PROXYHOST" ).getString();
int proxyPort = 80;
if ( containsAttribute( "PROXYPORT" ) ){
proxyPort = getDynamic( _Session, "PROXYPORT" ).getInt();
}
String proxyUser = null;
String proxyPassword = null;
if ( containsAttribute( "PROXYUSER" ) ){
proxyUser = getDynamic( _Session, "PROXYUSER" ).getString();
proxyPassword = getDynamic( _Session, "PROXYPASSWORD" ).getString();
}
settings.setProxyDetails( proxyHost, proxyPort, proxyUser, proxyPassword );
}
return settings;
}
@SuppressWarnings("deprecation")
private String handleDocument( cfSession _Session, InputStream _in, String _charset ) throws IOException, dataNotSupportedException, cfmRunTimeException{
String mimeType = getDynamic( _Session, "MIMETYPE" ).getString().toLowerCase();
String charset = _charset;
if ( charset == null ){
charset = "ISO-8859-1";
}
if ( mimeType.equals( "text/html" ) ){
return IOUtils.toString( _in, charset );
}else if ( mimeType.equals( "text/plain" ) ){
String plainTxt = IOUtils.toString( _in, charset );
return "<pre>" + escapeHtmlChars(plainTxt) + "</pre>";
}else if ( mimeType.startsWith( "image/" ) ){
File tmpFile = File.createTempFile( "cfdoc", '.' + mimeType.substring( mimeType.indexOf( '/' )+1 ) );
OutputStream fout = cfEngine.thisPlatform.getFileIO().getFileOutputStream( tmpFile );
StreamUtil.copyTo( _in, fout );
return "<img src=\"" + tmpFile.toURL() + "\"/>";
}else{
throw newRunTimeException( "Invalid MIMETYPE value. Supported values include text/html, text/plain, image/jpg, image/gif, image/png and image/bmp" );
}
}
/*
* escapeHTMLChars
*
* This is used to escape the HTML chars in a plain text file so
* that JTidy and flying saucer ignore them.
*/
private static String escapeHtmlChars(String content)
{
char[] old = new char[] { '&', '<', '>', '\"' };
String[] replacements = new String[] { "&", "<", ">", """ };
int strLen = content.length();
int charsLen = old.length;
StringBuilder buffer = new StringBuilder(content);
StringBuilder writer = new StringBuilder(strLen);
char nextChar;
boolean foundCh;
for (int i = 0; i < strLen; i++)
{
nextChar = buffer.charAt(i);
foundCh = false;
for (int j = 0; j < charsLen; j++)
{
if (nextChar == old[j])
{
writer.append(replacements[j]);
foundCh = true;
}
}
if (!foundCh)
{
writer.append(nextChar);
}
}
return writer.toString();//.Replace("\n", "<br>");
}
private void setPermissions( cfSession _Session, PDFEncryption _pdfEnc ) throws cfmRunTimeException{
// apply encryption
String encryption = getDynamic( _Session, "ENCRYPTION" ).getString().toLowerCase();
if ( encryption.equals( "40" ) || encryption.equals( "40-bit" ) ){
_pdfEnc.setEncryptionType( PdfWriter.STANDARD_ENCRYPTION_40);
}else if ( encryption.equals( "128" ) || encryption.equals( "128-bit" ) ){
_pdfEnc.setEncryptionType( PdfWriter.STANDARD_ENCRYPTION_128);
}else if ( encryption.equals( "aes" ) ){
_pdfEnc.setEncryptionType( PdfWriter.ENCRYPTION_AES_128);
}else if ( !encryption.equals( "none" ) ){
throw newRunTimeException( "Invalid ENCRYPTION value. Supported values include \"40-bit\", \"128-bit\", \"AES\" and \"none\"" );
}
// Default to no permissions
int permissionsMask = 0;
if ( containsAttribute( "PERMISSIONS" ) ){
String [] permissions = getDynamic( _Session, "PERMISSIONS" ).getString().toLowerCase().split( "," );
if ( permissions.length > 0 ){
for ( int i = 0; i < permissions.length; i++ ){
String nextPermission = permissions[i];
if ( nextPermission.equals( "allowprinting" ) ){
permissionsMask |= PdfWriter.ALLOW_PRINTING;
}else if ( nextPermission.equals( "allowmodifycontents" ) ){
permissionsMask |= PdfWriter.ALLOW_MODIFY_CONTENTS;
}else if ( nextPermission.equals( "allowcopy" ) ){
permissionsMask |= PdfWriter.ALLOW_COPY;
}else if ( nextPermission.equals( "allowmodifyannotations" ) ){
permissionsMask |= PdfWriter.ALLOW_MODIFY_ANNOTATIONS;
}else if ( nextPermission.equals( "allowscreenreaders" ) ){
if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
throw newRunTimeException("AllowScreenReaders is not valid with 40-bit encryption");
permissionsMask |= PdfWriter.ALLOW_SCREENREADERS;
}else if ( nextPermission.equals( "allowassembly" ) ){
if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
throw newRunTimeException("AllowAssembly is not valid with 40-bit encryption");
permissionsMask |= PdfWriter.ALLOW_ASSEMBLY;
}else if ( nextPermission.equals( "allowdegradedprinting" ) ){
if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
throw newRunTimeException("AllowDegradedPrinting is not valid with 40-bit encryption");
permissionsMask |= PdfWriter.ALLOW_DEGRADED_PRINTING;
}else if ( nextPermission.equals( "allowfillin" ) ){
if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40)
throw newRunTimeException("AllowFillIn is not valid with 40-bit encryption");
permissionsMask |= PdfWriter.ALLOW_FILL_IN;
}else{
throw newRunTimeException( "Invalid permissions value: " + nextPermission );
}
}
}
}
// Set the allowed permissions
_pdfEnc.setAllowedPrivileges(permissionsMask);
if ( containsAttribute("OWNERPASSWORD") )
_pdfEnc.setOwnerPassword( getDynamic( _Session, "OWNERPASSWORD" ).getString().getBytes() );
if ( containsAttribute("USERPASSWORD") )
_pdfEnc.setUserPassword( getDynamic( _Session, "USERPASSWORD" ).getString().getBytes() );
}
/*
* removeBackground
*
* Removes all of the background items like the 'bgcolor' attribute from
* the XHTML body.
*/
private String removeBackground(String _body)
{
try
{
// Parse the XHTML body into an XML object
cfXmlData content = cfXmlData.parseXml(_body, true, null);
// Remove the background items from the top node and all of its children
Node node = content.getXMLNode();
removeBackground(node);
// Convert the XML object back into a string
_body = content.toString();
}
catch (Exception e)
{
}
return _body;
}
/*
* removeBackground
*
* Removes all of the background items like the 'bgcolor' attribute from
* the XML node and recursively calls itself to remove them from all child
* nodes too.
*/
private void removeBackground(Node _node)
{
// Remove any background items from the node.
// For now we only remove the 'bgcolor' attribute.
NamedNodeMap attributes = _node.getAttributes();
if ( (attributes != null) && (attributes.getNamedItem("bgcolor") != null) )
attributes.removeNamedItem("bgcolor");
// If the node has children then make recursive calls to remove the
// background items from the children too.
if (_node.hasChildNodes())
{
NodeList children =_node.getChildNodes();
for (int i = 0; i < children.getLength(); i++)
removeBackground(children.item(i));
}
}
/*
* extractTagAttributes
*
* Extracts the attributes for the tag with the specified name.
*/
private HashMap<String, String> extractTagAttributes( String _tagName, String _xhtml )
{
HashMap<String, String> attributes = new HashMap<String, String>();
try
{
// Parse the XHTML body into an XML object
cfXmlData content = cfXmlData.parseXml(_xhtml, true, null);
// Find the node for the specified tag
Node node = findNode(content.getXMLNode(), _tagName);
if ( node != null)
{
// Copy the node's attributes (if any) into the HashMap
NamedNodeMap nodeAttributes = node.getAttributes();
if (nodeAttributes != null)
{
for (int i=0; i < nodeAttributes.getLength(); i++)
attributes.put(nodeAttributes.item(i).getNodeName(), nodeAttributes.item(i).getNodeValue());
}
}
}
catch (Exception e)
{
}
return attributes;
}
/*
* findNode
*
* Search the node and all child nodes for the node with the specified name.
*/
private Node findNode(Node _node, String _tagName)
{
String name = _node.getNodeName();
if ((name != null) && name.equals(_tagName))
return _node;
if (_node.hasChildNodes())
{
NodeList children =_node.getChildNodes();
for (int i = 0; i < children.getLength(); i++)
{
Node n = findNode(children.item(i), _tagName);
if ( n != null)
return n;
}
}
return null;
}
private String insertStyles( cfSession _Session, String _body, DocumentSection _section, DocumentSettings _settings, boolean _backgroundVisible, int _numSections ) throws cfmRunTimeException{
int headStart = _body.indexOf( "<head>" );
if ( headStart < 0 ){
return _body;
}
HashMap<String, String> bodyTagAttributes = null;
if ( !_backgroundVisible )
{
// The background is set to not visible so remove all
// the background items like the 'bgcolor' attribute.
_body = removeBackground(_body);
headStart = _body.indexOf( "<head>" );
}
else
{
// For some reason the BODY bgcolor attribute is being ignored so let's extract
// it and set the background color using CSS. We'll extract all the body tag's
// attributes since we might need them in the future.
bodyTagAttributes = extractTagAttributes("body", _body);
}
StringBuilder styleBlock = new StringBuilder();
styleBlock.append( "<style>\n" );
styleBlock.append( "@page{\n" );
styleBlock.append( getMargins( _Session, _section, _settings ) );
String header = replaceHeaderFooterVariables(_section, _section.getHeader(), _numSections);
String footer = replaceHeaderFooterVariables(_section, _section.getFooter(), _numSections);
insertHeaderFooter( _Session, styleBlock, "top", _section.getHeaderAlign(), header, _backgroundVisible, _section.getMimeType() );
insertHeaderFooter( _Session, styleBlock, "bottom", _section.getFooterAlign(), footer, _backgroundVisible, _section.getMimeType() );
styleBlock.append( "size: " );
styleBlock.append( _settings.getPageSize() );
styleBlock.append( ";\n" );
styleBlock.append( "}\n" );
if ((bodyTagAttributes != null) && ( bodyTagAttributes.containsKey("bgcolor")))
{
styleBlock.append( "body { background-color: " );
styleBlock.append( bodyTagAttributes.get("bgcolor") );
styleBlock.append( "; }\n");
}
styleBlock.append( "</style>\n" );
return _body.substring( 0, headStart+6 ) + styleBlock + _body.substring( headStart+6 );
}
/*
* replaceHeaderFooterVariables
*
* Replace the header/footer page count variables with the appropriate values.
*/
private String replaceHeaderFooterVariables(DocumentSection _section, String _content, int _numSections)
{
if (_content == null)
return null;
_content = _content.replaceAll("BD:CURRENTPAGENUMBER", "\" counter(page) \"");
_content = _content.replaceAll("BD:CURRENTSECTIONPAGENUMBER", "\" counter(page) \"");
if ( _numSections == 1 ){
// There's only 1 section so we can use the page and pages counters.
_content = _content.replaceAll("BD:TOTALPAGECOUNT", "\" counter(pages) \"");
_content = _content.replaceAll("BD:TOTALSECTIONPAGECOUNT", "\" counter(pages) \"");
} else {
// There's more than 1 section so we need to use the values calculated by preparePageCounters().
_content = _content.replaceAll("BD:TOTALPAGECOUNT", Integer.toString(_section.getTotalPageCount()));
_content = _content.replaceAll("BD:TOTALSECTIONPAGECOUNT", Integer.toString(_section.getTotalSectionPageCount()));
}
return _content;
}
private void insertHeaderFooter( cfSession _Session, StringBuilder _sb, String _position, String _align, String _content, boolean _backgroundVisible, String _sectionMimeType ) throws cfmRunTimeException{
if ( _content != null ){
_sb.append( "@" );
_sb.append( _position );
_sb.append( "-" );
_sb.append( _align );
_sb.append( "{\nwhite-space: pre;\n" ); // this is necessary so escaped newlines in the content are displayed properly
// If the section had a mimetype specified and it's text/plain or if it didn't have a mimetype
// specified and the document mimetype was set to text/plain then treat the body as plain text.
if ((( _sectionMimeType != null ) && (_sectionMimeType.equalsIgnoreCase("text/plain"))) ||
(( _sectionMimeType == null ) && (getDynamic(_Session,"MIMETYPE").getString().equalsIgnoreCase("text/plain"))))
{
_sb.append( "content: " );
_sb.append( convertPlainTextToCSSContent(_content) );
}
else
{
HashMap<String, String> properties = new HashMap<String,String>();
String xhtml = getHeaderFooterXHTML(_content, properties);
if ( _backgroundVisible && properties.containsKey("background-color"))
{
_sb.append( "background-color: " );
_sb.append( properties.get("background-color") );
_sb.append( ";\n" );
}
_sb.append( "content: " );
_sb.append( xhtml );
}
_sb.append( ";\n}\n" );
}
}
private String getHeaderFooterXHTML(String _content, HashMap<String,String> _properties)
{
// Convert to XHTML
String xhtml = getXHTML(_content);
// Find the beginning of the body tag and body text (starts with double quotes)
int beginBody = xhtml.indexOf("<body");
if ( beginBody >= 0 )
{
beginBody = xhtml.indexOf('>',beginBody);
while ( xhtml.charAt(beginBody) != '"' )
beginBody++;
}
else
{
beginBody = 0;
}
// Find the end of the body text (ends with double quotes) and extract the body
String body;
int endPos = xhtml.indexOf("</body>");
if ( endPos > 0)
{
while ( xhtml.charAt(endPos) != '"' )
endPos--;
body = xhtml.substring(beginBody, endPos+1);
}
else
{
body = xhtml.substring(beginBody);
}
// Convert the body to an appropriate format for CSS content
String cssContent = convertHTMLToCSSContent(body);
// Now see if there are any HTML attributes that need to be returned
// as CSS properties.
HashMap<String, String> attributes = extractTagAttributes("body", xhtml);
if ( attributes.containsKey("bgcolor"))
_properties.put("background-color", attributes.get("bgcolor"));
return cssContent;
}
/*
* convertPlainTextToCSSContent
*/
private static String convertPlainTextToCSSContent(String _text)
{
StringBuilder sb = new StringBuilder();
// First escape all HTML special characters except for the surrounding double quotes
_text = '"' + escapeHtmlChars(_text.substring(1,_text.length()-1)) + '"';
// Now escape all newline characters and HTML escaped double quotes
for ( int i = 0; i < _text.length(); i++ )
{
char ch = _text.charAt(i);
switch (ch)
{
case '\r':
case '\n':
sb.append("\\A "); // escape sequence for newline
if ( (ch == '\r') && (i+1 < _text.length()) && (_text.charAt(i+1) == '\n') )
i++;
break;
case '&':
if ( _text.startsWith(""",i) )
{
if ( _text.startsWith("" counter(page) "",i) )
{
sb.append("\" counter(page) \""); // put back as unescaped
i += "" counter(page) "".length() - 1; // move to the end
}
else if ( _text.startsWith("" counter(pages) "",i) )
{
sb.append("\" counter(pages) \""); // put back as unescaped
i += "" counter(pages) "".length() - 1; // move to the end
}
else
{
sb.append("\\22 "); // escape sequence for double quote
i += """.length() - 1; // move to the end
}
}
else
{
sb.append(ch);
}
break;
default:
sb.append(ch);
break;
}
}
return sb.toString();
}
/*
* convertHTMLToCSSContent
*/
private static String convertHTMLToCSSContent(String _html)
{
StringBuilder sb = new StringBuilder();
for ( int i = 0; i < _html.length(); i++ )
{
char ch = _html.charAt(i);
switch (ch)
{
case '\r':
case '\n':
// Do nothing so these characters are removed.
break;
case '<':
int endTag = _html.indexOf('>',i);
if ( endTag > i )
{
String htmlTag = _html.substring(i, endTag+1);
// Currently we only handle the <br> tag.
if ( htmlTag.equals("<br />"))
sb.append("\\A "); // escape sequence for newline
i = endTag;
}
else
{
sb.append(ch);
}
break;
default:
sb.append(ch);
break;
}
}
return sb.toString();
}
private String getMargins( cfSession _Session, DocumentSection _section, DocumentSettings _settings ) throws dataNotSupportedException, cfmRunTimeException{
StringBuilder sb = new StringBuilder();
String marginTop = _section.getMarginTop();
if ( _section.getMarginTop() == null ){
marginTop = _settings.getMarginTop();
}
if ( marginTop != null ){
sb.append( "margin-top: " );
sb.append( marginTop );
sb.append( _settings.getUnit() );
sb.append( ";\n" );
}
String marginBottom = _section.getMarginBottom();
if ( _section.getMarginBottom() == null ){
marginBottom = _settings.getMarginBottom();
}
if ( marginBottom != null ){
sb.append( "margin-bottom: " );
sb.append( marginBottom );
sb.append( _settings.getUnit() );
sb.append( ";\n" );
}
String marginLeft = _section.getMarginLeft();
if ( _section.getMarginLeft() == null ){
marginLeft = _settings.getMarginLeft();
}
if ( marginLeft != null ){
sb.append( "margin-left: " );
sb.append( marginLeft );
sb.append( _settings.getUnit() );
sb.append( ";\n" );
}
String marginRight = _section.getMarginRight();
if ( _section.getMarginRight() == null ){
marginRight = _settings.getMarginRight();
}
if ( marginRight != null ){
sb.append( "margin-right: " );
sb.append( marginRight );
sb.append( _settings.getUnit() );
sb.append( ";\n" );
}
return sb.toString();
}
private void appendSectionAttributes( cfSession _Session, DocumentSection _section ) throws cfmRunTimeException{
if ( containsAttribute( "MIMETYPE" ) )
_section.setMimeType( getDynamic( _Session, "MIMETYPE" ).toString() );
if ( containsAttribute( "NAME" ) )
_section.setName( getDynamic( _Session, "NAME" ).toString() );
//TODO: validate values?
if ( containsAttribute( "MARGINTOP" ) )
_section.setMarginTop( getDynamic( _Session, "MARGINTOP" ).toString() );
if ( containsAttribute( "MARGINBOTTOM" ) )
_section.setMarginBottom( getDynamic( _Session, "MARGINBOTTOM" ).toString() );
if ( containsAttribute( "MARGINLEFT" ) )
_section.setMarginLeft( getDynamic( _Session, "MARGINLEFT" ).toString() );
if ( containsAttribute( "MARGINRIGHT" ) )
_section.setMarginRight( getDynamic( _Session, "MARGINRIGHT" ).toString() );
_section.setUserAgent( getDynamic( _Session, "USERAGENT" ).getString() );
if ( containsAttribute( "AUTHPASSWORD" ) && containsAttribute( "AUTHUSER" ) ){
_section.setAuthentication( getDynamic( _Session, "AUTHUSER" ).getString(), getDynamic( _Session, "AUTHPASSWORD" ).getString() );
}
}
private class SessionOutputStream extends java.io.OutputStream{
private cfSession session;
private cfmRunTimeException exception;
private boolean firstWrite = true;
private String saveAsName;
SessionOutputStream( cfSession _session, String _saveAsName ){
session = _session;
saveAsName = _saveAsName;
}
public cfmRunTimeException getException(){
return exception;
}
@Override
public void write( byte[] b, int off, int len ) throws IOException {
if ( exception == null ){ // only attempt to write further if exception hasn't occurred
try {
// If this is the first write then set the content type and appropriate headers.
// We wait to set these here so that any exceptions that occur before this will
// be displayed properly in the browser.
if ( firstWrite ){
firstWrite = false;
session.setContentType( "application/pdf" );
session.setHeader("Content-Disposition", "inline; filename=" + saveAsName);
}
session.write( b, off, len );
} catch (cfmRunTimeException e) {
exception = e;
}
}
}
@Override
public void write( byte[] b ) throws IOException {
this.write( b, 0, b.length );
}
@Override
public void write( int arg0 ) throws IOException {
this.write( new byte[]{ (byte) arg0 }, 0, 1 );
}
}
private class NullOutputStream extends java.io.OutputStream{
NullOutputStream(){
}
@Override
public void write( byte[] b, int off, int len ) throws IOException {
}
@Override
public void write( byte[] b ) throws IOException {
}
@Override
public void write( int arg0 ) throws IOException {
}
}
private class CreationListener extends DefaultPDFCreationListener
{
private PdfString author;
private PdfString title;
private PdfString subject;
private PdfString keywords;
public CreationListener(cfData _author, cfData _title, cfData _subject, cfData _keywords)
{
if ( _author != null )
author = new PdfString(_author.toString());
if ( _title != null )
title = new PdfString(_title.toString());
if ( _subject != null )
subject = new PdfString(_subject.toString());
if ( _keywords != null )
keywords = new PdfString(_keywords.toString());
}
//public void preOpen(ITextRenderer renderer)
public void onClose(ITextRenderer renderer)
{
PdfString creator = new PdfString("OpenBD " + cfEngine.PRODUCT_VERSION + " (" + cfEngine.BUILD_ISSUE + ")");
renderer.getOutputDevice().getWriter().getInfo().put(PdfName.CREATOR,creator);
if (author != null)
renderer.getOutputDevice().getWriter().getInfo().put(PdfName.AUTHOR,author);
if (title != null)
renderer.getOutputDevice().getWriter().getInfo().put(PdfName.TITLE,title);
if (subject != null)
renderer.getOutputDevice().getWriter().getInfo().put(PdfName.SUBJECT,subject);
if (keywords != null)
renderer.getOutputDevice().getWriter().getInfo().put(PdfName.KEYWORDS,keywords);
}
}
}