001: /*
002: * ============================================================================
003: * GNU Lesser General Public License
004: * ============================================================================
005: *
006: * JasperReports - Free Java report-generating library.
007: * Copyright (C) 2001-2006 JasperSoft Corporation http://www.jaspersoft.com
008: *
009: * This library is free software; you can redistribute it and/or
010: * modify it under the terms of the GNU Lesser General Public
011: * License as published by the Free Software Foundation; either
012: * version 2.1 of the License, or (at your option) any later version.
013: *
014: * This library is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017: * Lesser General Public License for more details.
018: *
019: * You should have received a copy of the GNU Lesser General Public
020: * License along with this library; if not, write to the Free Software
021: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
022: *
023: * JasperSoft Corporation
024: * 303 Second Street, Suite 450 North
025: * San Francisco, CA 94107
026: * http://www.jaspersoft.com
027: */
028: package net.sf.jasperreports.engine.export;
029:
030: import java.io.File;
031: import java.io.FileOutputStream;
032: import java.io.IOException;
033: import java.io.OutputStream;
034: import java.io.OutputStreamWriter;
035: import java.io.StringWriter;
036: import java.io.Writer;
037: import java.util.Arrays;
038: import java.util.List;
039: import java.util.StringTokenizer;
040:
041: import net.sf.jasperreports.engine.JRAbstractExporter;
042: import net.sf.jasperreports.engine.JRAlignment;
043: import net.sf.jasperreports.engine.JRException;
044: import net.sf.jasperreports.engine.JRExporterParameter;
045: import net.sf.jasperreports.engine.JRPrintFrame;
046: import net.sf.jasperreports.engine.JRPrintPage;
047: import net.sf.jasperreports.engine.JRPrintText;
048: import net.sf.jasperreports.engine.JasperPrint;
049: import net.sf.jasperreports.engine.util.JRStyledText;
050: import net.sf.jasperreports.engine.util.JRStyledTextParser;
051:
052: import org.xml.sax.SAXException;
053:
054: /**
055: * Exports filled reports in plain text format. The text exporter allows users to define a custom character resolution
056: * (the number of columns and rows in text format). Since the character resolution is mapped on the actual pixel resolution,
057: * every character corresponds to a rectangle of pixels. If a certain text element has a smaller size in pixels (width,
058: * height, or both) than the number of pixels that map to a character, the text element will not be rendered. Because of
059: * this, users must take some precautions when creating reports for text export. First, they must make sure the page size in
060: * characters is large enough to render the report, because if the report pages contain too much text, some of it may be
061: * rendered only partially. On the other hand, if the character resolution is too small compared to the pixel resolution
062: * (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
063: * character, will not be displayed (in the previous examplle, a text element needs to be at least 800/20 = 40 pixels tall
064: * in order to be rendered).
065: * <p>
066: * As a conclusion, the text exporter will yield the better results if the space needed for displaying a text is large. So
067: * users have to either design reports with few text or export to big text pages. Another good practice is to arrange text
068: * elements at design time as similar as possible to a grid.
069: *
070: * @see JRExporterParameter
071: * @author Ionut Nedelcu (ionutned@users.sourceforge.net)
072: * @version $Id: JRTextExporter.java 1824 2007-08-23 14:19:12Z teodord $
073: */
074: public class JRTextExporter extends JRAbstractExporter {
075: protected int pageWidth;
076: protected int pageHeight;
077: protected int characterHeight;
078: protected int characterWidth;
079: protected JRExportProgressMonitor progressMonitor;
080: protected Writer writer;
081: char[][] pageData;
082: protected String betweenPagesText;
083: protected String lineSeparator;
084:
085: protected static final String systemLineSeparator = System
086: .getProperty("line.separator");
087: /**
088: *
089: */
090: protected JRStyledTextParser styledTextParser = new JRStyledTextParser();
091:
092: /**
093: *
094: */
095: public void exportReport() throws JRException {
096: progressMonitor = (JRExportProgressMonitor) parameters
097: .get(JRExporterParameter.PROGRESS_MONITOR);
098:
099: /* */
100: setOffset();
101:
102: /* */
103: setInput();
104:
105: /* */
106: if (!isModeBatch) {
107: setPageRange();
108: }
109:
110: String encoding = getStringParameterOrDefault(
111: JRExporterParameter.CHARACTER_ENCODING,
112: JRExporterParameter.PROPERTY_CHARACTER_ENCODING);
113:
114: Integer characterWidthParam = (Integer) parameters
115: .get(JRTextExporterParameter.CHARACTER_WIDTH);
116: if (characterWidthParam != null) {
117: characterWidth = characterWidthParam.intValue();
118: if (characterWidth < 0)
119: throw new JRException(
120: "Character width must be greater than 0");
121: } else {
122: Integer pageWidthParam = (Integer) parameters
123: .get(JRTextExporterParameter.PAGE_WIDTH);
124: if (pageWidthParam != null) {
125: pageWidth = pageWidthParam.intValue();
126: if (pageWidth <= 0)
127: throw new JRException(
128: "Page width must be greater than 0");
129: } else {
130: throw new JRException(
131: "Character or page width must be specified");
132: }
133: }
134:
135: Integer characterHeightParam = (Integer) parameters
136: .get(JRTextExporterParameter.CHARACTER_HEIGHT);
137: if (characterHeightParam != null) {
138: characterHeight = characterHeightParam.intValue();
139: if (characterHeight < 0)
140: throw new JRException(
141: "Character height must be greater than 0");
142: } else {
143: Integer pageHeightParam = (Integer) parameters
144: .get(JRTextExporterParameter.PAGE_HEIGHT);
145: if (pageHeightParam != null) {
146: pageHeight = pageHeightParam.intValue();
147: if (pageHeight <= 0)
148: throw new JRException(
149: "Page height must be greater than 0");
150: } else {
151: throw new JRException(
152: "Character or page height must be specified");
153: }
154: }
155:
156: betweenPagesText = (String) parameters
157: .get(JRTextExporterParameter.BETWEEN_PAGES_TEXT);
158: if (betweenPagesText == null) {
159: betweenPagesText = systemLineSeparator
160: + systemLineSeparator;
161: }
162:
163: lineSeparator = (String) parameters
164: .get(JRTextExporterParameter.LINE_SEPARATOR);
165: if (lineSeparator == null) {
166: lineSeparator = systemLineSeparator;
167: }
168:
169: StringBuffer sb = (StringBuffer) parameters
170: .get(JRExporterParameter.OUTPUT_STRING_BUFFER);
171: if (sb != null) {
172: try {
173: writer = new StringWriter();
174: exportReportToWriter();
175: sb.append(writer.toString());
176: } catch (IOException e) {
177: throw new JRException(
178: "Error writing to StringBuffer writer : "
179: + jasperPrint.getName(), e);
180: } finally {
181: if (writer != null) {
182: try {
183: writer.close();
184: } catch (IOException e) {
185: }
186: }
187: }
188: } else {
189: writer = (Writer) parameters
190: .get(JRExporterParameter.OUTPUT_WRITER);
191: if (writer != null) {
192: try {
193: exportReportToWriter();
194: } catch (IOException e) {
195: throw new JRException("Error writing to writer : "
196: + jasperPrint.getName(), e);
197: }
198: } else {
199: OutputStream os = (OutputStream) parameters
200: .get(JRExporterParameter.OUTPUT_STREAM);
201: if (os != null) {
202: try {
203: writer = new OutputStreamWriter(os, encoding);
204: exportReportToWriter();
205: } catch (IOException e) {
206: throw new JRException(
207: "Error writing to OutputStream writer : "
208: + jasperPrint.getName(), e);
209: }
210: } else {
211: File destFile = (File) parameters
212: .get(JRExporterParameter.OUTPUT_FILE);
213: if (destFile == null) {
214: String fileName = (String) parameters
215: .get(JRExporterParameter.OUTPUT_FILE_NAME);
216: if (fileName != null) {
217: destFile = new File(fileName);
218: } else {
219: throw new JRException(
220: "No output specified for the exporter.");
221: }
222: }
223:
224: try {
225: os = new FileOutputStream(destFile);
226: writer = new OutputStreamWriter(os, encoding);
227: exportReportToWriter();
228: } catch (IOException e) {
229: throw new JRException(
230: "Error writing to file writer : "
231: + jasperPrint.getName(), e);
232: } finally {
233: if (writer != null) {
234: try {
235: writer.close();
236: } catch (IOException e) {
237: }
238: }
239: }
240: }
241: }
242: }
243: }
244:
245: /**
246: *
247: */
248: protected void exportReportToWriter() throws JRException,
249: IOException {
250: for (int reportIndex = 0; reportIndex < jasperPrintList.size(); reportIndex++) {
251: jasperPrint = (JasperPrint) jasperPrintList
252: .get(reportIndex);
253:
254: List pages = jasperPrint.getPages();
255: if (pages != null && pages.size() > 0) {
256: if (isModeBatch) {
257: startPageIndex = 0;
258: endPageIndex = pages.size() - 1;
259: }
260:
261: if (characterWidth > 0)
262: pageWidth = jasperPrint.getPageWidth()
263: / characterWidth;
264: if (characterHeight > 0)
265: pageHeight = jasperPrint.getPageHeight()
266: / characterHeight;
267:
268: for (int i = startPageIndex; i <= endPageIndex; i++) {
269: if (Thread.currentThread().isInterrupted()) {
270: throw new JRException(
271: "Current thread interrupted.");
272: }
273:
274: JRPrintPage page = (JRPrintPage) pages.get(i);
275:
276: /* */
277: exportPage(page);
278: }
279: }
280: }
281:
282: writer.flush();
283: }
284:
285: /**
286: * Exports a page to the output writer. Only text elements within the page are considered. For each page, the engine
287: * creates a matrix of characters and each rendered text element is placed at the appropiate position in the matrix.
288: * After all texts are parsed, the character matrix is sent to the output writer.
289: */
290: protected void exportPage(JRPrintPage page) throws IOException {
291: List elements = page.getElements();
292:
293: pageData = new char[pageHeight][];
294: for (int i = 0; i < pageHeight; i++) {
295: pageData[i] = new char[pageWidth];
296: Arrays.fill(pageData[i], ' ');
297: }
298:
299: exportElements(elements);
300:
301: for (int i = 0; i < pageHeight; i++) {
302: writer.write(pageData[i]);
303: writer.write(lineSeparator);
304: }
305:
306: writer.write(betweenPagesText);
307:
308: if (progressMonitor != null) {
309: progressMonitor.afterPageExport();
310: }
311: }
312:
313: protected void exportElements(List elements) {
314: for (int i = 0; i < elements.size(); i++) {
315: Object element = elements.get(i);
316: if (element instanceof JRPrintText) {
317: exportText((JRPrintText) element);
318: } else if (element instanceof JRPrintFrame) {
319: JRPrintFrame frame = (JRPrintFrame) element;
320: setFrameElementsOffset(frame, false);
321: try {
322: exportElements(frame.getElements());
323: } finally {
324: restoreElementOffsets();
325: }
326: }
327: }
328: }
329:
330: /**
331: * Renders a text and places it in the output matrix.
332: */
333: protected void exportText(JRPrintText element) {
334: int rowCount = calculateYCoord(element.getHeight());
335: int columnCount = calculateXCoord(element.getWidth());
336: int x = calculateXCoord(element.getX() + getOffsetX());
337: int y = calculateYCoord(element.getY() + getOffsetY());
338:
339: if (x + columnCount > pageWidth) {
340: //if the text exceeds the page width, truncate the column count
341: columnCount = pageWidth - x;
342: }
343:
344: String allText;
345: JRStyledText styledText = getStyledText(element);
346: if (styledText == null) {
347: allText = "";
348: } else {
349: allText = styledText.getText();
350: }
351:
352: // if the space is too small, the element will not be rendered
353: if (rowCount <= 0 || columnCount <= 0)
354: return;
355:
356: if (allText != null && allText.length() == 0)
357: return;
358:
359: // uses an array of string buffers, since the maximum number of rows is already calculated
360: StringBuffer[] rows = new StringBuffer[rowCount];
361: rows[0] = new StringBuffer();
362: int rowIndex = 0;
363: int rowPosition = 0;
364:
365: // first search for \n, because it causes immediate line break
366: StringTokenizer lfTokenizer = new StringTokenizer(allText, "\n");
367: label: while (lfTokenizer.hasMoreTokens()) {
368: String line = lfTokenizer.nextToken();
369: StringTokenizer spaceTokenizer = new StringTokenizer(line,
370: " ", true);
371:
372: // divide each text line in words
373: while (spaceTokenizer.hasMoreTokens()) {
374: String word = spaceTokenizer.nextToken();
375:
376: // situation: word is larger than the entire column
377: // in this case breaking occurs in the middle of the word
378: while (word.length() > columnCount) {
379: rows[rowIndex].append(word.substring(0, columnCount
380: - rowPosition));
381: word = word.substring(columnCount - rowPosition,
382: word.length());
383: rowIndex++;
384: if (rowIndex == rowCount)
385: break label;
386: rowPosition = 0;
387: rows[rowIndex] = new StringBuffer();
388: }
389:
390: // situation: word is larger than remaining space on the current line
391: // in this case, go to the next line
392: if (rowPosition + word.length() > columnCount) {
393: rowIndex++;
394: if (rowIndex == rowCount)
395: break label;
396: rowPosition = 0;
397: rows[rowIndex] = new StringBuffer();
398: }
399:
400: // situation: the word is actually a space and it situated at the beginning of a new line
401: // in this case, it is removed
402: if (rowIndex > 9 && rowPosition == 0
403: && word.equals(" "))
404: break;
405:
406: // situation: the word is small enough to fit in the current line
407: // in this case just add the word and increment the cursor position
408: rows[rowIndex].append(word);
409: rowPosition += word.length();
410: }
411:
412: rowIndex++;
413: if (rowIndex == rowCount)
414: break;
415: rowPosition = 0;
416: rows[rowIndex] = new StringBuffer();
417: }
418:
419: int xOffset = 0;
420: int yOffset = 0;
421:
422: if (element.getVerticalAlignment() == JRAlignment.VERTICAL_ALIGN_BOTTOM)
423: yOffset = rowCount - rowIndex;
424: if (element.getVerticalAlignment() == JRAlignment.VERTICAL_ALIGN_MIDDLE)
425: yOffset = (rowCount - rowIndex) / 2;
426:
427: for (int i = 0; i < rowIndex; i++) {
428: String line = rows[i].toString();
429: int pos = line.length() - 1;
430: while (pos >= 0 && line.charAt(pos) == ' ')
431: pos--;
432: line = line.substring(0, pos + 1);
433: if (element.getHorizontalAlignment() == JRAlignment.HORIZONTAL_ALIGN_RIGHT)
434: xOffset = columnCount - line.length();
435: if (element.getHorizontalAlignment() == JRAlignment.HORIZONTAL_ALIGN_CENTER)
436: xOffset = (columnCount - line.length()) / 2;
437:
438: // if text is justified, there is no offset, but the line text is modified
439: // the last line in the paragraph is not justified.
440: if (element.getHorizontalAlignment() == JRAlignment.HORIZONTAL_ALIGN_JUSTIFIED)
441: if (i < rowIndex - 1)
442: line = justifyText(line, columnCount);
443:
444: char[] chars = line.toCharArray();
445: System.arraycopy(chars, 0, pageData[y + yOffset + i], x
446: + xOffset, chars.length);
447: }
448: }
449:
450: /**
451: * Justifies the text inside a specified space.
452: */
453: private String justifyText(String s, int width) {
454: StringBuffer justified = new StringBuffer();
455:
456: StringTokenizer t = new StringTokenizer(s, " ");
457: int tokenCount = t.countTokens();
458: if (tokenCount <= 1)
459: return s;
460:
461: String[] words = new String[tokenCount];
462: int i = 0;
463: while (t.hasMoreTokens())
464: words[i++] = t.nextToken();
465:
466: int emptySpace = width - s.length() + (words.length - 1);
467: int spaceCount = emptySpace / (words.length - 1);
468: int remainingSpace = emptySpace % (words.length - 1);
469:
470: char[] spaces = new char[spaceCount];
471: Arrays.fill(spaces, ' ');
472:
473: for (i = 0; i < words.length - 1; i++) {
474: justified.append(words[i]);
475: justified.append(spaces);
476: if (i < remainingSpace)
477: justified.append(' ');
478: }
479: justified.append(words[words.length - 1]);
480:
481: return justified.toString();
482: }
483:
484: /**
485: * Transforms y coordinates from pixel space to character space.
486: */
487: protected int calculateYCoord(int y) {
488: return pageHeight * y / jasperPrint.getPageHeight();
489: }
490:
491: /**
492: * Transforms x coordinates from pixel space to character space.
493: */
494: protected int calculateXCoord(int x) {
495: return pageWidth * x / jasperPrint.getPageWidth();
496: }
497:
498: /**
499: *
500: */
501: protected JRStyledText getStyledText(JRPrintText textElement) {
502: JRStyledText styledText = null;
503:
504: String text = textElement.getText();
505: if (text != null) {
506: if (textElement.isStyledText()) {
507: try {
508: styledText = styledTextParser.parse(null, text);
509: } catch (SAXException e) {
510: //ignore if invalid styled text and treat like normal text
511: }
512: }
513:
514: if (styledText == null) {
515: styledText = new JRStyledText();
516: styledText.append(text);
517: styledText.addRun(new JRStyledText.Run(null, 0, text
518: .length()));
519: }
520: }
521:
522: return styledText;
523: }
524:
525: }
|