/*
* JasperReports - Free Java Reporting Library.
* Copyright (C) 2001 - 2009 Jaspersoft Corporation. All rights reserved.
* http://www.jaspersoft.com
*
* Unless you have purchased a commercial license agreement from Jaspersoft,
* the following license terms apply:
*
* This program is part of JasperReports.
*
* JasperReports is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* JasperReports 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.jasperreports.engine.export;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;
import net.sf.jasperreports.engine.JRAbstractExporter;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExporterParameter;
import net.sf.jasperreports.engine.JRPrintFrame;
import net.sf.jasperreports.engine.JRPrintPage;
import net.sf.jasperreports.engine.JRPrintText;
import net.sf.jasperreports.engine.JRStyledTextAttributeSelector;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.util.JRProperties;
import net.sf.jasperreports.engine.util.JRStyledText;
/**
* Exports filled reports in plain text format. The text exporter allows users to define a custom character resolution
* (the number of columns and rows in text format). Since the character resolution is mapped on the actual pixel resolution,
* every character corresponds to a rectangle of pixels. If a certain text element has a smaller size in pixels (width,
* height, or both) than the number of pixels that map to a character, the text element will not be rendered. Because of
* this, users must take some precautions when creating reports for text export. First, they must make sure the page size in
* characters is large enough to render the report, because if the report pages contain too much text, some of it may be
* rendered only partially. On the other hand, if the character resolution is too small compared to the pixel resolution
* (say, a height of 20 characters for a page 800 pixels tall) all texts with sizes smaller than the one needed to map to a
* character, will not be displayed (in the previous example, a text element needs to be at least 800/20 = 40 pixels tall
* in order to be rendered).
* <p>
* As a conclusion, the text exporter will yield the better results if the space needed for displaying a text is large. So
* users have to either design reports with few text or export to big text pages. Another good practice is to arrange text
* elements at design time as similar as possible to a grid.
*
* @see JRExporterParameter
* @author Ionut Nedelcu (ionutned@users.sourceforge.net)
* @version $Id: JRTextExporter.java 3678 2010-04-02 12:13:06Z shertage $
*/
public class JRTextExporter extends JRAbstractExporter
{
private static final String TXT_EXPORTER_PROPERTIES_PREFIX = JRProperties.PROPERTY_PREFIX + "export.txt.";
protected int pageWidthInChars;
protected int pageHeightInChars;
protected float charWidth;
protected float charHeight;
protected JRExportProgressMonitor progressMonitor;
protected Writer writer;
char[][] pageData;
protected String betweenPagesText;
protected String lineSeparator;
protected static final String systemLineSeparator = System.getProperty("line.separator");
/**
*
*/
public void exportReport() throws JRException
{
progressMonitor = (JRExportProgressMonitor)parameters.get(JRExporterParameter.PROGRESS_MONITOR);
/* */
setOffset();
/* */
setInput();
if (!parameters.containsKey(JRExporterParameter.FILTER))
{
filter = createFilter(TXT_EXPORTER_PROPERTIES_PREFIX);
}
/* */
if (!isModeBatch)
{
setPageRange();
}
String encoding =
getStringParameterOrDefault(
JRExporterParameter.CHARACTER_ENCODING,
JRExporterParameter.PROPERTY_CHARACTER_ENCODING
);
betweenPagesText = (String) parameters.get(JRTextExporterParameter.BETWEEN_PAGES_TEXT);
if (betweenPagesText == null)
{
betweenPagesText = systemLineSeparator + systemLineSeparator;
}
lineSeparator = (String) parameters.get(JRTextExporterParameter.LINE_SEPARATOR);
if (lineSeparator == null)
{
lineSeparator = systemLineSeparator;
}
StringBuffer sb = (StringBuffer)parameters.get(JRExporterParameter.OUTPUT_STRING_BUFFER);
if (sb != null)
{
try
{
writer = new StringWriter();
exportReportToWriter();
sb.append(writer.toString());
}
catch (IOException e)
{
throw new JRException("Error writing to StringBuffer writer : " + jasperPrint.getName(), e);
}
finally
{
if (writer != null)
{
try
{
writer.close();
}
catch(IOException e)
{
}
}
}
}
else
{
writer = (Writer)parameters.get(JRExporterParameter.OUTPUT_WRITER);
if (writer != null)
{
try
{
exportReportToWriter();
}
catch (IOException e)
{
throw new JRException("Error writing to writer : " + jasperPrint.getName(), e);
}
}
else
{
OutputStream os = (OutputStream)parameters.get(JRExporterParameter.OUTPUT_STREAM);
if (os != null)
{
try
{
writer = new OutputStreamWriter(os, encoding);
exportReportToWriter();
}
catch (IOException e)
{
throw new JRException("Error writing to OutputStream writer : " + jasperPrint.getName(), e);
}
}
else
{
File destFile = (File)parameters.get(JRExporterParameter.OUTPUT_FILE);
if (destFile == null)
{
String fileName = (String)parameters.get(JRExporterParameter.OUTPUT_FILE_NAME);
if (fileName != null)
{
destFile = new File(fileName);
}
else
{
throw new JRException("No output specified for the exporter.");
}
}
try
{
os = new FileOutputStream(destFile);
writer = new OutputStreamWriter(os, encoding);
exportReportToWriter();
}
catch (IOException e)
{
throw new JRException("Error writing to file writer : " + jasperPrint.getName(), e);
}
finally
{
if (writer != null)
{
try
{
writer.close();
}
catch(IOException e)
{
}
}
}
}
}
}
}
/**
*
*/
public void setReportParameters() throws JRException
{
charWidth =
getFloatParameter(
JRTextExporterParameter.CHARACTER_WIDTH,
JRTextExporterParameter.PROPERTY_CHARACTER_WIDTH,
0
);
if (charWidth < 0)
{
throw new JRException("Character width in pixels must be greater than zero.");
}
else if (charWidth == 0)
{
pageWidthInChars =
getIntegerParameter(
JRTextExporterParameter.PAGE_WIDTH,
JRTextExporterParameter.PROPERTY_PAGE_WIDTH,
0
);
if (pageWidthInChars <= 0)
{
throw new JRException("Character width in pixels or page width in characters must be specified and must be greater than zero.");
}
charWidth = jasperPrint.getPageWidth() / (float)pageWidthInChars;
}
else
{
pageWidthInChars = (int)(jasperPrint.getPageWidth() / charWidth);
}
charHeight =
getFloatParameter(
JRTextExporterParameter.CHARACTER_HEIGHT,
JRTextExporterParameter.PROPERTY_CHARACTER_HEIGHT,
0
);
if (charHeight < 0)
{
throw new JRException("Character height in pixels must be greater than zero.");
}
else if (charHeight == 0)
{
pageHeightInChars =
getIntegerParameter(
JRTextExporterParameter.PAGE_HEIGHT,
JRTextExporterParameter.PROPERTY_PAGE_HEIGHT,
0
);
if (pageHeightInChars <= 0)
{
throw new JRException("Character height in pixels or page height in characters must be specified and must be greater than zero.");
}
charHeight = jasperPrint.getPageHeight() / (float)pageHeightInChars;
}
else
{
pageHeightInChars = (int)(jasperPrint.getPageHeight() / charHeight);
}
}
/**
*
*/
protected void exportReportToWriter() throws JRException, IOException
{
for(int reportIndex = 0; reportIndex < jasperPrintList.size(); reportIndex++)
{
setJasperPrint((JasperPrint)jasperPrintList.get(reportIndex));
List pages = jasperPrint.getPages();
if (pages != null && pages.size() > 0)
{
if (isModeBatch)
{
startPageIndex = 0;
endPageIndex = pages.size() - 1;
}
/* */
setReportParameters();//FIXMENOW check all report level exporter hints and make sure they are read from the current report, not from the first
for(int i = startPageIndex; i <= endPageIndex; i++)
{
if (Thread.interrupted())
{
throw new JRException("Current thread interrupted.");
}
JRPrintPage page = (JRPrintPage)pages.get(i);
/* */
exportPage(page);
}
}
}
writer.flush();
}
/**
* Exports a page to the output writer. Only text elements within the page are considered. For each page, the engine
* creates a matrix of characters and each rendered text element is placed at the appropiate position in the matrix.
* After all texts are parsed, the character matrix is sent to the output writer.
*/
protected void exportPage(JRPrintPage page) throws IOException
{
List elements = page.getElements();
pageData = new char[pageHeightInChars][];
for (int i = 0; i < pageHeightInChars; i++) {
pageData[i] = new char[pageWidthInChars];
Arrays.fill(pageData[i], ' ');
}
exportElements(elements);
for (int i = 0; i < pageHeightInChars; i++) {
writer.write(pageData[i]);
writer.write(lineSeparator);
}
writer.write(betweenPagesText);
if (progressMonitor != null)
{
progressMonitor.afterPageExport();
}
}
protected void exportElements(List elements)
{
for (int i = 0; i < elements.size();i++)
{
Object element = elements.get(i);
if (element instanceof JRPrintText)
{
exportText((JRPrintText) element);
}
else if (element instanceof JRPrintFrame)
{
JRPrintFrame frame = (JRPrintFrame) element;
setFrameElementsOffset(frame, false);
try
{
exportElements(frame.getElements());
}
finally
{
restoreElementOffsets();
}
}
}
}
/**
* Renders a text and places it in the output matrix.
*/
protected void exportText(JRPrintText element)
{
int colSpan = getWidthInChars(element.getWidth());
int rowSpan = getHeightInChars(element.getHeight());
int col = getWidthInChars(element.getX() + getOffsetX());
int row = getHeightInChars(element.getY() + getOffsetY());
if (col + colSpan > pageWidthInChars)
{
//if the text exceeds the page width, truncate the column count
colSpan = pageWidthInChars - col;
}
String allText;
JRStyledText styledText = getStyledText(element);
if (styledText == null)
{
allText = "";
}
else
{
allText = styledText.getText();
}
// if the space is too small, the element will not be rendered
if (rowSpan <= 0 || colSpan <= 0)
{
return;
}
if (allText != null && allText.length() == 0)
{
return;
}
// uses an array of string buffers, since the maximum number of rows is already calculated
StringBuffer[] rows = new StringBuffer[rowSpan];
rows[0] = new StringBuffer();
int rowIndex = 0;
int rowPosition = 0;
boolean isFirstLine = true;
// first search for \n, because it causes immediate line break
StringTokenizer lfTokenizer = new StringTokenizer(allText, "\n", true);
label:while (lfTokenizer.hasMoreTokens()) {
String line = lfTokenizer.nextToken();
// if text starts with a new line:
if(isFirstLine && line.equals("\n"))
{
rows[rowIndex].append("");
rowIndex++;
if(rowIndex == rowSpan || !lfTokenizer.hasMoreTokens())
{
break label;
}
rowPosition = 0;
rows[rowIndex] = new StringBuffer();
line = lfTokenizer.nextToken();
}
isFirstLine = false;
// if there is a series of new lines:
int emptyLinesCount = 0;
while(line.equals("\n") && lfTokenizer.hasMoreTokens())
{
emptyLinesCount ++;
line = lfTokenizer.nextToken();
}
if(emptyLinesCount > 1)
{
for(int i = 0; i < emptyLinesCount-1; i++)
{
rows[rowIndex].append("");
rowIndex++;
if(rowIndex == rowSpan)
{
break label;
}
rowPosition = 0;
rows[rowIndex] = new StringBuffer();
//if this is the last empty line:
if(!lfTokenizer.hasMoreTokens() && line.equals("\n"))
{
rows[rowIndex].append("");
break label;
}
}
}
if(!line.equals("\n"))
{
StringTokenizer spaceTokenizer = new StringTokenizer(line, " ", true);
// divide each text line in words
while (spaceTokenizer.hasMoreTokens()) {
String word = spaceTokenizer.nextToken();
// situation: word is larger than the entire column
// in this case breaking occurs in the middle of the word
while (word.length() > colSpan) {
rows[rowIndex].append(word.substring(0, colSpan - rowPosition));
word = word.substring(colSpan - rowPosition, word.length());
rowIndex++;
if(rowIndex == rowSpan)
{
break label;
}
rowPosition = 0;
rows[rowIndex] = new StringBuffer();
}
// situation: word is larger than remaining space on the current line
// in this case, go to the next line
if (rowPosition + word.length() > colSpan)
{
rowIndex++;
if (rowIndex == rowSpan)
{
break label;
}
rowPosition = 0;
rows[rowIndex] = new StringBuffer();
}
// situation: the word is actually a space and it situated at the beginning of a new line
// in this case, it is removed
if (rowIndex > 0 && rowPosition == 0 && word.equals(" "))
{
continue;
}
// situation: the word is small enough to fit in the current line
// in this case just add the word and increment the cursor position
rows[rowIndex].append(word);
rowPosition += word.length();
}
rowIndex++;
if(rowIndex == rowSpan)
{
break;
}
rowPosition = 0;
rows[rowIndex] = new StringBuffer();
}
}
int colOffset = 0;
int rowOffset = 0;
switch (element.getVerticalAlignmentValue())
{
case BOTTOM :
{
rowOffset = rowSpan - rowIndex;
break;
}
case MIDDLE :
{
rowOffset = (rowSpan - rowIndex) / 2;
break;
}
}
for (int i = 0; i < rowIndex; i++) {
String line = rows[i].toString();
int pos = line.length() - 1;
while (pos >= 0 && line.charAt(pos) == ' ')
{
pos--;
}
line = line.substring(0, pos + 1);
switch (element.getHorizontalAlignmentValue())
{
case RIGHT :
{
colOffset = colSpan - line.length();
break;
}
case CENTER :
{
colOffset = (colSpan - line.length()) / 2;
break;
}
// if text is justified, there is no offset, but the line text is modified
// the last line in the paragraph is not justified.
case JUSTIFIED :
{
if (i < rowIndex -1)
{
line = justifyText(line, colSpan);
}
break;
}
}
char[] chars = line.toCharArray();
System.arraycopy(chars, 0, pageData[row + rowOffset + i], col + colOffset, chars.length);
}
}
/**
* Justifies the text inside a specified space.
*/
private String justifyText(String s, int width)
{
StringBuffer justified = new StringBuffer();
StringTokenizer t = new StringTokenizer(s, " ");
int tokenCount = t.countTokens();
if (tokenCount <= 1)
{
return s;
}
String[] words = new String[tokenCount];
int i = 0;
while (t.hasMoreTokens())
{
words[i++] = t.nextToken();
}
int emptySpace = width - s.length() + (words.length - 1);
int spaceCount = emptySpace / (words.length - 1);
int remainingSpace = emptySpace % (words.length - 1);
char[] spaces = new char[spaceCount];
Arrays.fill(spaces, ' ');
for (i = 0; i < words.length - 1; i++)
{
justified.append(words[i]);
justified.append(spaces);
if (i < remainingSpace)
{
justified.append(' ');
}
}
justified.append(words[words.length-1]);
return justified.toString();
}
/**
* Transforms height from pixel space to character space.
*/
protected int getHeightInChars(int height)
{
//return (int) (((long) pageHeightInChars * height) / jasperPrint.getPageHeight());
return Math.round(height / charHeight);
}
/**
* Transforms width from pixel space to character space.
*/
protected int getWidthInChars(int width)
{
// return pageWidthInChars * width / jasperPrint.getPageWidth();
return Math.round(width / charWidth);
}
/**
*
*/
protected JRStyledText getStyledText(JRPrintText textElement)
{
return textElement.getStyledText(JRStyledTextAttributeSelector.NONE);
}
/**
*
*/
protected String getExporterKey()
{
return null;
}
}