/* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ==================================================================== */ package org.apache.poi.xssf.streaming; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Iterator; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.FormulaError; import org.apache.poi.ss.util.CellReference; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.poi.util.TempFile; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.xssf.usermodel.XSSFRichTextString; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STCellType; /** * Initially copied from BigGridDemo "SpreadsheetWriter". * Unlike the original code which wrote the entire document, * this class only writes the "sheetData" document fragment * so that it was renamed to "SheetDataWriter" */ public class SheetDataWriter { private static final POILogger logger = POILogFactory.getLogger(SheetDataWriter.class); private final File _fd; private final Writer _out; private int _rownum; private int _numberOfFlushedRows; private int _lowestIndexOfFlushedRows; // meaningful only of _numberOfFlushedRows>0 private int _numberOfCellsOfLastFlushedRow; // meaningful only of _numberOfFlushedRows>0 private int _numberLastFlushedRow = -1; // meaningful only of _numberOfFlushedRows>0 /** * Table of strings shared across this workbook. * If two cells contain the same string, then the cell value is the same index into SharedStringsTable */ private SharedStringsTable _sharedStringSource; public SheetDataWriter() throws IOException { _fd = createTempFile(); _out = createWriter(_fd); } public SheetDataWriter(SharedStringsTable sharedStringsTable) throws IOException { this(); this._sharedStringSource = sharedStringsTable; } /** * Create a temp file to write sheet data. * By default, temp files are created in the default temporary-file directory * with a prefix "poi-sxssf-sheet" and suffix ".xml". Subclasses can override * it and specify a different temp directory or filename or suffix, e.g. <code>.gz</code> * * @return temp file to write sheet data */ public File createTempFile() throws IOException { return TempFile.createTempFile("poi-sxssf-sheet", ".xml"); } /** * Create a writer for the sheet data. * * @param fd the file to write to */ public Writer createWriter(File fd) throws IOException { FileOutputStream fos = new FileOutputStream(fd); OutputStream decorated; try { decorated = decorateOutputStream(fos); } catch (final IOException e) { fos.close(); throw e; } return new BufferedWriter( new OutputStreamWriter(decorated, "UTF-8")); } /** * Override this to translate (such as encrypt or compress) the file output stream * as it is being written to disk. * The default behavior is to to pass the stream through unmodified. * * @param fos the stream to decorate * @return a decorated stream * @throws IOException * @see #decorateInputStream(FileInputStream) */ protected OutputStream decorateOutputStream(FileOutputStream fos) throws IOException { return fos; } /** * flush and close the temp data writer. * This method <em>must</em> be invoked before calling {@link #getWorksheetXMLInputStream()} */ public void close() throws IOException{ _out.flush(); _out.close(); } protected File getTempFile(){ return _fd; } /** * @return a stream to read temp file with the sheet data */ public InputStream getWorksheetXMLInputStream() throws IOException { File fd = getTempFile(); FileInputStream fis = new FileInputStream(fd); try { return decorateInputStream(fis); } catch (IOException e) { fis.close(); throw e; } } /** * Override this to translate (such as decrypt or expand) the file input stream * as it is being read from disk. * The default behavior is to to pass the stream through unmodified. * * @param fis the stream to decorate * @return a decorated stream * @throws IOException * @see #decorateOutputStream(FileOutputStream) */ protected InputStream decorateInputStream(FileInputStream fis) throws IOException { return fis; } public int getNumberOfFlushedRows() { return _numberOfFlushedRows; } public int getNumberOfCellsOfLastFlushedRow() { return _numberOfCellsOfLastFlushedRow; } public int getLowestIndexOfFlushedRows() { return _lowestIndexOfFlushedRows; } public int getLastFlushedRow() { return _numberLastFlushedRow; } @Override protected void finalize() throws Throwable { if (!_fd.delete()) { logger.log(POILogger.ERROR, "Can't delete temporary encryption file: "+_fd); } super.finalize(); } /** * Write a row to the file * * @param rownum 0-based row number * @param row a row */ public void writeRow(int rownum, SXSSFRow row) throws IOException { if (_numberOfFlushedRows == 0) _lowestIndexOfFlushedRows = rownum; _numberLastFlushedRow = Math.max(rownum, _numberLastFlushedRow); _numberOfCellsOfLastFlushedRow = row.getLastCellNum(); _numberOfFlushedRows++; beginRow(rownum, row); Iterator<Cell> cells = row.allCellsIterator(); int columnIndex = 0; while (cells.hasNext()) { writeCell(columnIndex++, cells.next()); } endRow(); } void beginRow(int rownum, SXSSFRow row) throws IOException { _out.write("<row r=\"" + (rownum + 1) + "\""); if (row.hasCustomHeight()) _out.write(" customHeight=\"true\" ht=\"" + row.getHeightInPoints() + "\""); if (row.getZeroHeight()) _out.write(" hidden=\"true\""); if (row.isFormatted()) { _out.write(" s=\"" + row.getRowStyleIndex() + "\""); _out.write(" customFormat=\"1\""); } if (row.getOutlineLevel() != 0) { _out.write(" outlineLevel=\"" + row.getOutlineLevel() + "\""); } if(row.getHidden() != null) { _out.write(" hidden=\"" + (row.getHidden() ? "1" : "0") + "\""); } if(row.getCollapsed() != null) { _out.write(" collapsed=\"" + (row.getCollapsed() ? "1" : "0") + "\""); } _out.write(">\n"); this._rownum = rownum; } void endRow() throws IOException { _out.write("</row>\n"); } public void writeCell(int columnIndex, Cell cell) throws IOException { if (cell == null) { return; } String ref = new CellReference(_rownum, columnIndex).formatAsString(); _out.write("<c r=\"" + ref + "\""); CellStyle cellStyle = cell.getCellStyle(); if (cellStyle.getIndex() != 0) { // need to convert the short to unsigned short as the indexes can be up to 64k // ideally we would use int for this index, but that would need changes to some more // APIs _out.write(" s=\"" + (cellStyle.getIndex() & 0xffff) + "\""); } CellType cellType = cell.getCellTypeEnum(); switch (cellType) { case BLANK: { _out.write(">"); break; } case FORMULA: { _out.write(">"); _out.write("<f>"); outputQuotedString(cell.getCellFormula()); _out.write("</f>"); switch (cell.getCachedFormulaResultTypeEnum()) { case NUMERIC: double nval = cell.getNumericCellValue(); if (!Double.isNaN(nval)) { _out.write("<v>" + nval + "</v>"); } break; default: break; } break; } case STRING: { if (_sharedStringSource != null) { XSSFRichTextString rt = new XSSFRichTextString(cell.getStringCellValue()); int sRef = _sharedStringSource.addEntry(rt.getCTRst()); _out.write(" t=\"" + STCellType.S + "\">"); _out.write("<v>"); _out.write(String.valueOf(sRef)); _out.write("</v>"); } else { _out.write(" t=\"inlineStr\">"); _out.write("<is><t"); if (hasLeadingTrailingSpaces(cell.getStringCellValue())) { _out.write(" xml:space=\"preserve\""); } _out.write(">"); outputQuotedString(cell.getStringCellValue()); _out.write("</t></is>"); } break; } case NUMERIC: { _out.write(" t=\"n\">"); _out.write("<v>" + cell.getNumericCellValue() + "</v>"); break; } case BOOLEAN: { _out.write(" t=\"b\">"); _out.write("<v>" + (cell.getBooleanCellValue() ? "1" : "0") + "</v>"); break; } case ERROR: { FormulaError error = FormulaError.forInt(cell.getErrorCellValue()); _out.write(" t=\"e\">"); _out.write("<v>" + error.getString() + "</v>"); break; } default: { throw new IllegalStateException("Invalid cell type: " + cellType); } } _out.write("</c>"); } /** * @return whether the string has leading / trailing spaces that * need to be preserved with the xml:space=\"preserve\" attribute */ boolean hasLeadingTrailingSpaces(String str) { if (str != null && str.length() > 0) { char firstChar = str.charAt(0); char lastChar = str.charAt(str.length() - 1); return Character.isWhitespace(firstChar) || Character.isWhitespace(lastChar) ; } return false; } //Taken from jdk1.3/src/javax/swing/text/html/HTMLWriter.java protected void outputQuotedString(String s) throws IOException { if (s == null || s.length() == 0) { return; } char[] chars = s.toCharArray(); int last = 0; int length = s.length(); for (int counter = 0; counter < length; counter++) { char c = chars[counter]; switch (c) { case '<': if (counter > last) { _out.write(chars, last, counter - last); } last = counter + 1; _out.write("<"); break; case '>': if (counter > last) { _out.write(chars, last, counter - last); } last = counter + 1; _out.write(">"); break; case '&': if (counter > last) { _out.write(chars, last, counter - last); } last = counter + 1; _out.write("&"); break; case '"': if (counter > last) { _out.write(chars, last, counter - last); } last = counter + 1; _out.write("""); break; // Special characters case '\n': case '\r': if (counter > last) { _out.write(chars, last, counter - last); } _out.write(" "); last = counter + 1; break; case '\t': if (counter > last) { _out.write(chars, last, counter - last); } _out.write(" "); last = counter + 1; break; case 0xa0: if (counter > last) { _out.write(chars, last, counter - last); } _out.write(" "); last = counter + 1; break; default: // YK: XmlBeans silently replaces all ISO control characters ( < 32) with question marks. // the same rule applies to unicode surrogates and "not a character" symbols. if( c < ' ' || Character.isLowSurrogate(c) || Character.isHighSurrogate(c) || ('\uFFFE' <= c && c <= '\uFFFF')) { if (counter > last) { _out.write(chars, last, counter - last); } _out.write('?'); last = counter + 1; } else if (c > 127) { if (counter > last) { _out.write(chars, last, counter - last); } last = counter + 1; // If the character is outside of ascii, write the // numeric value. _out.write("&#"); _out.write(String.valueOf((int) c)); _out.write(";"); } break; } } if (last < length) { _out.write(chars, last, length - last); } } /** * Deletes the temporary file that backed this sheet on disk. * @return true if the file was deleted, false if it wasn't. */ boolean dispose() throws IOException { final boolean ret; try { _out.close(); } finally { ret = _fd.delete(); } return ret; } }