/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2006 - 2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.modules.output.table.html.helper; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.text.NumberFormat; import org.pentaho.reporting.engine.classic.core.AttributeNames; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; import org.pentaho.reporting.engine.classic.core.ClassicEngineInfo; import org.pentaho.reporting.engine.classic.core.ReportAttributeMap; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.CellBackground; import org.pentaho.reporting.engine.classic.core.modules.output.table.base.SlimSheetLayout; import org.pentaho.reporting.engine.classic.core.modules.output.table.html.HtmlPrinter; import org.pentaho.reporting.engine.classic.core.modules.output.table.html.HtmlTableModule; import org.pentaho.reporting.engine.classic.core.modules.output.table.html.URLRewriteException; import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.util.MemoryStringReader; import org.pentaho.reporting.libraries.fonts.encoding.EncodingRegistry; import org.pentaho.reporting.libraries.repository.ContentIOException; import org.pentaho.reporting.libraries.repository.ContentItem; import org.pentaho.reporting.libraries.repository.ContentLocation; import org.pentaho.reporting.libraries.repository.NameGenerator; import org.pentaho.reporting.libraries.resourceloader.ResourceManager; import org.pentaho.reporting.libraries.xmlns.common.AttributeList; import org.pentaho.reporting.libraries.xmlns.writer.XmlWriter; import org.pentaho.reporting.libraries.xmlns.writer.XmlWriterSupport; @SuppressWarnings( "HardCodedStringLiteral" ) public abstract class AbstractHtmlPrinter { public static final String XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; protected static final String[] XHTML_HEADER = { "<!DOCTYPE html", " PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"", " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" }; protected static final StyleBuilder.CSSKeys[] EMPTY_CELL_ATTRNAMES = new StyleBuilder.CSSKeys[] { StyleBuilder.CSSKeys.FONT_SIZE }; protected static final String[] EMPTY_CELL_ATTRVALS = new String[] { "1pt" }; private static final String GENERATOR = ClassicEngineInfo.getInstance().getName() + " version " + ClassicEngineInfo.getInstance().getVersion(); private DefaultStyleBuilderFactory styleBuilderFactory; private DefaultHtmlContentGenerator contentGenerator; private Configuration configuration; private ContentItem styleFile; private String styleFileUrl; private HtmlTagHelper tagHelper; private boolean allowRawLinkTargets; public AbstractHtmlPrinter( ResourceManager resourceManager ) { if ( resourceManager == null ) { throw new NullPointerException( "A resource-manager must be given." ); } contentGenerator = new DefaultHtmlContentGenerator( resourceManager ); } protected void initialize( Configuration configuration ) { this.configuration = configuration; this.contentGenerator.setCopyExternalImages( "true".equals( configuration .getConfigProperty( HtmlTableModule.COPY_EXTERNAL_IMAGES ) ) ); this.allowRawLinkTargets = "true".equals( configuration.getConfigProperty( HtmlTableModule.ALLOW_RAW_LINK_TARGETS ) ); styleBuilderFactory = new DefaultStyleBuilderFactory(); styleBuilderFactory.configure( ClassicEngineBoot.getInstance().getGlobalConfig() ); this.tagHelper = new HtmlTagHelper( configuration, styleBuilderFactory ); } public boolean isAllowRawLinkTargets() { return allowRawLinkTargets; } public StyleManager getStyleManager() { return this.tagHelper.getStyleManager(); } public void setStyleManager( final StyleManager styleManager ) { this.tagHelper.setStyleManager( styleManager ); } public HtmlTagHelper getTagHelper() { return tagHelper; } public Configuration getConfiguration() { return configuration; } public void setDataWriter( final ContentLocation dataLocation, final NameGenerator dataNameGenerator ) { this.contentGenerator.setDataWriter( dataLocation, dataNameGenerator, getContentReWriteService() ); } protected abstract ContentUrlReWriteService getContentReWriteService(); public StyleBuilder getStyleBuilder() { return tagHelper.getStyleBuilder(); } public DefaultStyleBuilderFactory getStyleBuilderFactory() { return styleBuilderFactory; } public DefaultHtmlContentGenerator getContentGenerator() { return contentGenerator; } protected ResourceManager getResourceManager() { return contentGenerator.getResourceManager(); } protected boolean isProportionalColumnWidths() { return "true".equals( getConfiguration().getConfigProperty( HtmlTableModule.PROPORTIONAL_COLUMN_WIDTHS, "false" ) ); } protected void writeColumnDeclaration( final SlimSheetLayout sheetLayout, final XmlWriter xmlWriter ) throws IOException { StyleBuilder styleBuilder = getStyleBuilder(); DefaultStyleBuilderFactory styleBuilderFactory = getStyleBuilderFactory(); if ( sheetLayout == null ) { throw new NullPointerException(); } final int colCount = sheetLayout.getColumnCount(); final int fullWidth = (int) StrictGeomUtility.toExternalValue( sheetLayout.getMaxWidth() ); final String[] colWidths = new String[colCount]; final boolean proportionalColumnWidths = isProportionalColumnWidths(); final NumberFormat pointConverter = styleBuilder.getPointConverter(); final String unit; if ( proportionalColumnWidths ) { unit = "%"; double totalWidth = 0; for ( int col = 0; col < colCount; col++ ) { final int width = (int) StrictGeomUtility.toExternalValue( sheetLayout.getCellWidth( col, col + 1 ) ); final double colWidth = styleBuilderFactory.fixLengthForSafari( Math.max( 1, width * 100.0d / fullWidth ) ); if ( col == colCount - 1 ) { colWidths[col] = pointConverter.format( 100 - totalWidth ); } else { totalWidth += colWidth; colWidths[col] = pointConverter.format( colWidth ); } } } else { unit = "pt"; double totalWidth = 0; for ( int col = 0; col < colCount; col++ ) { final int width = (int) StrictGeomUtility.toExternalValue( sheetLayout.getCellWidth( col, col + 1 ) ); final double colWidth = styleBuilderFactory.fixLengthForSafari( Math.max( 1, width ) ); if ( col == colCount - 1 ) { colWidths[col] = pointConverter.format( fullWidth - totalWidth ); } else { totalWidth += colWidth; colWidths[col] = pointConverter.format( colWidth ); } } } for ( int col = 0; col < colCount; col++ ) { // Print the table. styleBuilder.clear(); styleBuilder.append( DefaultStyleBuilder.CSSKeys.WIDTH, colWidths[col], unit ); xmlWriter.writeTag( null, "col", "style", styleBuilder.toString(), XmlWriterSupport.CLOSE ); } } protected void writeCompleteHeader( final XmlWriter docWriter, final String sheetName, final ReportAttributeMap attributes, final String styleSheetUrl, final StyleManager inlineStyleSheet ) throws IOException { Configuration configuration = getConfiguration(); final String encoding = configuration.getConfigProperty( HtmlTableModule.ENCODING, EncodingRegistry.getPlatformDefaultEncoding() ); docWriter.writeXmlDeclaration( encoding ); for ( int i = 0; i < XHTML_HEADER.length; i++ ) { docWriter.writeText( XHTML_HEADER[i] ); docWriter.writeNewLine(); } docWriter.writeTag( XHTML_NAMESPACE, "html", XmlWriterSupport.OPEN ); docWriter.writeTag( XHTML_NAMESPACE, "head", XmlWriterSupport.OPEN ); final String title = configuration.getConfigProperty( HtmlTableModule.TITLE ); if ( title != null ) { docWriter.writeTag( XHTML_NAMESPACE, "title", XmlWriterSupport.OPEN ); docWriter.writeTextNormalized( title, false ); docWriter.writeCloseTag(); } else if ( sheetName != null ) { // if no single title defined, use the sheetname function previously computed docWriter.writeTag( XHTML_NAMESPACE, "title", XmlWriterSupport.OPEN ); docWriter.writeTextNormalized( sheetName, true ); docWriter.writeCloseTag(); } else { docWriter.writeTag( XHTML_NAMESPACE, "title", XmlWriterSupport.OPEN ); docWriter.writeText( " " ); docWriter.writeCloseTag(); } writeMeta( docWriter, "subject", configuration.getConfigProperty( HtmlTableModule.SUBJECT ) ); writeMeta( docWriter, "author", configuration.getConfigProperty( HtmlTableModule.AUTHOR ) ); writeMeta( docWriter, "keywords", configuration.getConfigProperty( HtmlTableModule.KEYWORDS ) ); writeMeta( docWriter, "generator", GENERATOR ); final AttributeList metaAttrs = new AttributeList(); metaAttrs.setAttribute( XHTML_NAMESPACE, "http-equiv", "content-type" ); metaAttrs.setAttribute( XHTML_NAMESPACE, "content", "text/html; charset=" + encoding ); docWriter.writeTag( XHTML_NAMESPACE, "meta", metaAttrs, XmlWriterSupport.CLOSE ); if ( styleSheetUrl != null ) { final AttributeList attrList = new AttributeList(); attrList.setAttribute( XHTML_NAMESPACE, "type", "text/css" ); attrList.setAttribute( XHTML_NAMESPACE, "rel", "stylesheet" ); attrList.setAttribute( XHTML_NAMESPACE, "href", styleSheetUrl ); docWriter.writeTag( XHTML_NAMESPACE, "link", attrList, XmlWriterSupport.CLOSE ); } else if ( inlineStyleSheet != null ) { docWriter.writeTag( XHTML_NAMESPACE, "style", "type", "text/css", XmlWriterSupport.OPEN ); StringWriter writer = new StringWriter(); inlineStyleSheet.write( writer ); docWriter.writeText( writer.toString() ); docWriter.writeCloseTag(); } final Object rawHeaderContent = attributes.getAttribute( AttributeNames.Html.NAMESPACE, AttributeNames.Html.EXTRA_RAW_HEADER_CONTENT ); if ( rawHeaderContent != null ) { // Warning: This text is not escaped or processed in any way. it is *RAW* content. docWriter.writeText( String.valueOf( rawHeaderContent ) ); } docWriter.writeCloseTag(); } private void writeMeta( final XmlWriter writer, final String name, final String value ) throws IOException { if ( value == null ) { return; } final AttributeList attrList = new AttributeList(); attrList.setAttribute( XHTML_NAMESPACE, "name", name ); attrList.setAttribute( XHTML_NAMESPACE, "content", value ); writer.writeTag( XHTML_NAMESPACE, "meta", attrList, XmlWriterSupport.CLOSE ); } protected StyleManager createStyleManager() { if ( isCreateBodyFragment() == false && isInlineStylesRequested() == false ) { return new GlobalStyleManager(); } return new InlineStyleManager(); } protected boolean isCreateBodyFragment() { return "true".equals( getConfiguration().getConfigProperty( HtmlTableModule.BODY_FRAGMENT, "false" ) ); } protected boolean isInlineStylesRequested() { return "true".equals( getConfiguration().getConfigProperty( HtmlTableModule.INLINE_STYLE ) ); } protected void generateHeaderOnOpen( final ReportAttributeMap attributeMap, final String sheetName, final XmlWriter xmlWriter ) throws IOException { if ( isCreateBodyFragment() == false ) { if ( isInlineStylesRequested() ) { writeCompleteHeader( xmlWriter, sheetName, attributeMap, null, null ); } else { if ( isExternalStyleSheetRequested() ) { if ( isForceBufferedWriting() == false ) { writeCompleteHeader( xmlWriter, sheetName, attributeMap, styleFileUrl, null ); } } } xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "body", XmlWriterSupport.OPEN ); } } protected void generateExternalStylePlaceHolder() throws ContentIOException, URLRewriteException { if ( isExternalStyleSheetRequested() == false ) { return; } this.styleFile = getContentGenerator().createItem( "style", "text/css" ); this.styleFileUrl = getContentReWriteService().rewriteContentDataItem( styleFile ); } public ContentItem getStyleFile() { return styleFile; } public String getStyleFileUrl() { return styleFileUrl; } protected WriterService createWriterService( final OutputStream out ) throws UnsupportedEncodingException { final String encoding = configuration.getConfigProperty( HtmlTableModule.ENCODING, EncodingRegistry.getPlatformDefaultEncoding() ); if ( isCreateBodyFragment() == false ) { if ( isInlineStylesRequested() ) { return WriterService.createPassThroughService( out, encoding ); } else { if ( isExternalStyleSheetRequested() && isForceBufferedWriting() == false ) { return WriterService.createPassThroughService( out, encoding ); } else { return WriterService.createBufferedService( out, encoding ); } } } else { return WriterService.createPassThroughService( out, encoding ); } } protected boolean isForceBufferedWriting() { return "true".equals( getConfiguration().getConfigProperty( HtmlTableModule.FORCE_BUFFER_WRITING ) ); } protected boolean isExternalStyleSheetRequested() { if ( isCreateBodyFragment() ) { // body-fragments have no header .. return false; } // We will add the style-declarations directly to the HTML elements .. if ( isInlineStylesRequested() ) { return false; } // Without the ability to create external files, we cannot create external stylesheet. if ( getContentGenerator().isExternalContentAvailable() == false ) { return false; } // User explicitly requested internal styles by disabling the external-style property. return "true".equals( getConfiguration().getConfigProperty( HtmlTableModule.EXTERNALIZE_STYLE, "true" ) ); } protected void performCloseFile( final String sheetName, final ReportAttributeMap logicalPageBox, final WriterService writer ) throws IOException, ContentIOException { XmlWriter xmlWriter = writer.getXmlWriter(); xmlWriter.writeCloseTag(); // for the opening table .. final Object rawFooterContent = logicalPageBox.getAttribute( AttributeNames.Html.NAMESPACE, AttributeNames.Html.EXTRA_RAW_FOOTER_CONTENT ); if ( rawFooterContent != null ) { xmlWriter.writeText( String.valueOf( rawFooterContent ) ); } if ( isCreateBodyFragment() ) { xmlWriter.close(); return; } ContentItem styleFile = getStyleFile(); if ( styleFile != null ) { final String encoding = getConfiguration() .getConfigProperty( HtmlTableModule.ENCODING, EncodingRegistry.getPlatformDefaultEncoding() ); final Writer styleOut = new OutputStreamWriter( new BufferedOutputStream( styleFile.getOutputStream() ), encoding ); getStyleManager().write( styleOut ); styleOut.flush(); styleOut.close(); if ( isForceBufferedWriting() == false ) { // A complete header had been written when the processing started .. xmlWriter.writeCloseTag(); // for the body tag xmlWriter.writeCloseTag(); // for the HTML tag xmlWriter.close(); return; } } if ( isInlineStylesRequested() ) { xmlWriter.writeCloseTag(); // for the body tag xmlWriter.writeCloseTag(); // for the HTML tag xmlWriter.close(); return; } // handle external stylesheets. They need to be injected into the header. // finish the body fragment xmlWriter.writeCloseTag(); // for the body .. xmlWriter.flush(); final XmlWriter docWriter = writer.createHeaderXmlWriter(); if ( styleFile != null ) { // now its time to write the header with the link to the style-sheet-file writeCompleteHeader( docWriter, sheetName, logicalPageBox, getStyleFileUrl(), null ); } else { writeCompleteHeader( docWriter, sheetName, logicalPageBox, null, getStyleManager() ); } // no need to check for IOExceptions here, as we know the implementation does not create such things final MemoryStringReader stringReader = writer.getBufferWriter().createReader(); docWriter.writeStream( stringReader ); stringReader.close(); docWriter.writeCloseTag(); // for the html .. docWriter.close(); } protected void openSheet( final ReportAttributeMap logicalPage, final String sheetName, final OutputProcessorMetaData metaData, final SlimSheetLayout sheetLayout, final XmlWriter xmlWriter ) throws ContentIOException, URLRewriteException, IOException { setStyleManager( createStyleManager() ); generateExternalStylePlaceHolder(); generateHeaderOnOpen( logicalPage, sheetName, xmlWriter ); final Object rawContent = logicalPage.getAttribute( AttributeNames.Html.NAMESPACE, AttributeNames.Html.EXTRA_RAW_CONTENT ); if ( rawContent != null ) { xmlWriter.writeText( String.valueOf( rawContent ) ); } // table name if ( "true".equals( metaData.getConfiguration().getConfigProperty( "org.pentaho.reporting.engine.classic.core.modules.output.table.html.EnableSheetNameProcessing" ) ) ) { if ( sheetName != null ) { xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "h1", getTagHelper().createSheetNameAttributes(), XmlWriterSupport.OPEN ); xmlWriter.writeTextNormalized( sheetName, true ); xmlWriter.writeCloseTag(); } } // table xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "table", getTagHelper().createTableAttributes( sheetLayout, logicalPage ), XmlWriterSupport.OPEN ); writeColumnDeclaration( sheetLayout, xmlWriter ); } protected void writeBackgroundCell( CellBackground background, XmlWriter xmlWriter ) throws IOException { final boolean emptyCellsUseCSS = getTagHelper().isEmptyCellsUseCSS(); if ( background == null ) { if ( emptyCellsUseCSS ) { xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "td", XmlWriterSupport.CLOSE ); } else { final AttributeList attrs = new AttributeList(); attrs.setAttribute( HtmlPrinter.XHTML_NAMESPACE, "style", "font-size: 1pt" ); xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "td", attrs, XmlWriterSupport.OPEN ); xmlWriter.writeText( " " ); xmlWriter.writeCloseTag(); } return; } StyleBuilder styleBuilder = getStyleBuilder(); DefaultStyleBuilderFactory styleBuilderFactory = getStyleBuilderFactory(); // Background cannot be null at this point .. final String[] anchor = background.getAnchors(); if ( anchor.length == 0 && emptyCellsUseCSS ) { final StyleBuilder cellStyle = styleBuilderFactory.createCellStyle( styleBuilder, null, null, background, null, null ); final AttributeList cellAttributes = getTagHelper().createCellAttributes( 1, 1, null, null, background, cellStyle ); xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "td", cellAttributes, XmlWriterSupport.CLOSE ); } else { final StyleBuilder cellStyle = styleBuilderFactory.createCellStyle( styleBuilder, null, null, background, HtmlPrinter.EMPTY_CELL_ATTRNAMES, HtmlPrinter.EMPTY_CELL_ATTRVALS ); final AttributeList cellAttributes = getTagHelper().createCellAttributes( 1, 1, null, null, background, cellStyle ); xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "td", cellAttributes, XmlWriterSupport.OPEN ); for ( int i = 0; i < anchor.length; i++ ) { xmlWriter.writeTag( HtmlPrinter.XHTML_NAMESPACE, "a", "name", anchor[i], XmlWriterSupport.CLOSE ); } xmlWriter.writeText( " " ); xmlWriter.writeCloseTag(); } } }