<action dev="POI-DEVELOPERS" type="add">Created a common interface for handling Excel files, irrespective of if they are .xls or .xlsx</action>
</release>
<release version="3.1.1-alpha1" date="2008-??-??">
+ <action dev="POI-DEVELOPERS" type="add">Support for HPBF Publisher hyperlinks, including during text extraction</action>
+ <action dev="POI-DEVELOPERS" type="fix">26321 and 44958 - preserve position of ArrayRecords and TableRecords among cell value records</action>
<action dev="POI-DEVELOPERS" type="fix">Impove empty header or footer handling in HWPF HeaderStories</action>
<action dev="POI-DEVELOPERS" type="fix">Avoid NPE in hssf.usermodel.HeaderFooter when stripping fields out</action>
<action dev="POI-DEVELOPERS" type="fix">Avoid NPE in EscherBSERecord on older escher records</action>
<action dev="POI-DEVELOPERS" type="add">Created a common interface for handling Excel files, irrespective of if they are .xls or .xlsx</action>
</release>
<release version="3.1.1-alpha1" date="2008-??-??">
+ <action dev="POI-DEVELOPERS" type="add">Support for HPBF Publisher hyperlinks, including during text extraction</action>
+ <action dev="POI-DEVELOPERS" type="fix">26321 and 44958 - preserve position of ArrayRecords and TableRecords among cell value records</action>
<action dev="POI-DEVELOPERS" type="fix">Impove empty header or footer handling in HWPF HeaderStories</action>
<action dev="POI-DEVELOPERS" type="fix">Avoid NPE in hssf.usermodel.HeaderFooter when stripping fields out</action>
<action dev="POI-DEVELOPERS" type="fix">Avoid NPE in EscherBSERecord on older escher records</action>
--- /dev/null
+/* ====================================================================
+ 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.hssf.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.poi.hssf.record.ArrayRecord;
+import org.apache.poi.hssf.record.MergeCellsRecord;
+import org.apache.poi.hssf.record.Record;
+import org.apache.poi.hssf.record.SharedFormulaRecord;
+import org.apache.poi.hssf.record.TableRecord;
+import org.apache.poi.hssf.record.aggregates.MergedCellsTable;
+import org.apache.poi.hssf.record.aggregates.SharedValueManager;
+
+/**
+ * Segregates the 'Row Blocks' section of a single sheet into plain row/cell records and
+ * shared formula records.
+ *
+ * @author Josh Micich
+ */
+public final class RowBlocksReader {
+
+ private final List _plainRecords;
+ private final SharedValueManager _sfm;
+ private final MergeCellsRecord[] _mergedCellsRecords;
+ private final int _totalNumberOfRecords;
+
+ /**
+ * Also collects any loose MergeCellRecords and puts them in the supplied
+ * mergedCellsTable
+ */
+ public RowBlocksReader(List recs, int startIx) {
+ List plainRecords = new ArrayList();
+ List shFrmRecords = new ArrayList();
+ List arrayRecords = new ArrayList();
+ List tableRecords = new ArrayList();
+ List mergeCellRecords = new ArrayList();
+
+ int endIx = -1;
+ for (int i = startIx; i < recs.size(); i++) {
+ Record rec = (Record) recs.get(i);
+ if (RecordOrderer.isEndOfRowBlock(rec.getSid())) {
+ // End of row/cell records for the current sheet
+ // Note - It is important that this code does not inadvertently any sheet
+ // records from a subsequent sheet. For example, if SharedFormulaRecords
+ // are taken from the wrong sheet, this could cause bug 44449.
+ endIx = i;
+ break;
+ }
+ List dest;
+ switch (rec.getSid()) {
+ case MergeCellsRecord.sid: dest = mergeCellRecords; break;
+ case SharedFormulaRecord.sid: dest = shFrmRecords; break;
+ case ArrayRecord.sid: dest = arrayRecords; break;
+ case TableRecord.sid: dest = tableRecords; break;
+ default: dest = plainRecords;
+ }
+ dest.add(rec);
+ }
+ if (endIx < 0) {
+ throw new RuntimeException("Failed to find end of row/cell records");
+ }
+ SharedFormulaRecord[] sharedFormulaRecs = new SharedFormulaRecord[shFrmRecords.size()];
+ ArrayRecord[] arrayRecs = new ArrayRecord[arrayRecords.size()];
+ TableRecord[] tableRecs = new TableRecord[tableRecords.size()];
+ shFrmRecords.toArray(sharedFormulaRecs);
+ arrayRecords.toArray(arrayRecs);
+ tableRecords.toArray(tableRecs);
+
+ _plainRecords = plainRecords;
+ _sfm = SharedValueManager.create(sharedFormulaRecs, arrayRecs, tableRecs);
+ _mergedCellsRecords = new MergeCellsRecord[mergeCellRecords.size()];
+ mergeCellRecords.toArray(_mergedCellsRecords);
+ _totalNumberOfRecords = endIx - startIx;
+ }
+
+ /**
+ * Some unconventional apps place {@link MergeCellsRecord}s within the row block. They
+ * actually should be in the {@link MergedCellsTable} which is much later (see bug 45699).
+ * @return any loose <tt>MergeCellsRecord</tt>s found
+ */
+ public MergeCellsRecord[] getLooseMergedCells() {
+ return _mergedCellsRecords;
+ }
+
+ public int getTotalNumberOfRecords() {
+ return _totalNumberOfRecords;
+ }
+
+ public SharedValueManager getSharedFormulaManager() {
+ return _sfm;
+ }
+ /**
+ * @return a {@link RecordStream} containing all the non-{@link SharedFormulaRecord}
+ * non-{@link ArrayRecord} and non-{@link TableRecord} Records.
+ */
+ public RecordStream getPlainRecordStream() {
+ return new RecordStream(_plainRecords, 0);
+ }
+}
import org.apache.poi.hssf.record.CalcModeRecord;
import org.apache.poi.hssf.record.CellValueRecordInterface;
import org.apache.poi.hssf.record.ColumnInfoRecord;
-import org.apache.poi.hssf.record.DBCellRecord;
import org.apache.poi.hssf.record.DVALRecord;
import org.apache.poi.hssf.record.DefaultColWidthRecord;
import org.apache.poi.hssf.record.DefaultRowHeightRecord;
import org.apache.poi.hssf.record.SaveRecalcRecord;
import org.apache.poi.hssf.record.ScenarioProtectRecord;
import org.apache.poi.hssf.record.SelectionRecord;
-import org.apache.poi.hssf.record.StringRecord;
import org.apache.poi.hssf.record.UncalcedRecord;
import org.apache.poi.hssf.record.WSBoolRecord;
import org.apache.poi.hssf.record.WindowTwoRecord;
import org.apache.poi.hssf.record.aggregates.ColumnInfoRecordsAggregate;
import org.apache.poi.hssf.record.aggregates.ConditionalFormattingTable;
import org.apache.poi.hssf.record.aggregates.DataValidityTable;
+import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate;
import org.apache.poi.hssf.record.aggregates.MergedCellsTable;
import org.apache.poi.hssf.record.aggregates.PageSettingsBlock;
import org.apache.poi.hssf.record.aggregates.RecordAggregate;
import org.apache.poi.hssf.record.aggregates.RowRecordsAggregate;
+import org.apache.poi.hssf.record.aggregates.RecordAggregate.PositionTrackingVisitor;
import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
import org.apache.poi.hssf.util.CellRangeAddress;
import org.apache.poi.hssf.util.PaneInformation;
private static POILogger log = POILogFactory.getLogger(Sheet.class);
protected ArrayList records = null;
- int preoffset = 0; // offset of the sheet in a new file
protected int dimsloc = -1; // TODO - remove dimsloc
protected PrintGridlinesRecord printGridlines = null;
protected GridsetRecord gridset = null;
* @see #createSheet(List,int,int)
*/
public Sheet() {
- _mergedCellsTable = new MergedCellsTable();
+ _mergedCellsTable = new MergedCellsTable();
}
/**
for (int k = offset; k < inRecs.size(); k++) {
Record rec = ( Record ) inRecs.get(k);
- if ( rec.getSid() == DBCellRecord.sid ) {
- continue;
- }
if ( rec.getSid() == IndexRecord.sid ) {
// ignore INDEX record because it is only needed by Excel,
// and POI always re-calculates its contents
continue;
}
- if ( rec.getSid() == StringRecord.sid ) {
- continue;
- }
-
+
if ( rec.getSid() == CFHeaderRecord.sid ) {
RecordStream rs = new RecordStream(inRecs, k);
retval.condFormatting = new ConditionalFormattingTable(rs);
if (retval._rowsAggregate != null) {
throw new RuntimeException("row/cell records found in the wrong place");
}
- int lastRowCellRec = findEndOfRowBlock(inRecs, k, retval._mergedCellsTable);
- retval._rowsAggregate = new RowRecordsAggregate(inRecs, k, lastRowCellRec);
+ RowBlocksReader rbr = new RowBlocksReader(inRecs, k);
+ retval._mergedCellsTable.addRecords(rbr.getLooseMergedCells());
+ retval._rowsAggregate = new RowRecordsAggregate(rbr.getPlainRecordStream(), rbr.getSharedFormulaManager());
records.add(retval._rowsAggregate); //only add the aggregate once
- k = lastRowCellRec -1;
+ k += rbr.getTotalNumberOfRecords() - 1;
continue;
}
}
if (rec.getSid() == MergeCellsRecord.sid) {
- // when the MergedCellsTable is found in the right place, we expect those records to be contiguous
+ // when the MergedCellsTable is found in the right place, we expect those records to be contiguous
RecordStream rs = new RecordStream(inRecs, k);
retval._mergedCellsTable.read(rs);
k += rs.getCountRead()-1;
continue;
}
-
+ if (rec.getSid() == UncalcedRecord.sid) {
+ // don't add UncalcedRecord to the list
+ retval._isUncalced = true; // this flag is enough
+ continue;
+ }
+
if (rec.getSid() == BOFRecord.sid)
{
bofEofNestingLevel++;
break;
}
}
- else if (rec.getSid() == UncalcedRecord.sid) {
- retval._isUncalced = true;
- }
else if (rec.getSid() == DimensionsRecord.sid)
{
// Make a columns aggregate if one hasn't ready been created.
return retval;
}
- /**
- * Also collects any rogue MergeCellRecords
- * @return the index one after the last row/cell record
- */
- private static int findEndOfRowBlock(List recs, int startIx, MergedCellsTable mergedCellsTable) {
- for(int i=startIx; i<recs.size(); i++) {
- Record rec = (Record) recs.get(i);
- if (RecordOrderer.isEndOfRowBlock(rec.getSid())) {
- return i;
- }
- if (rec.getSid() == MergeCellsRecord.sid) {
- // Some apps scatter these records between the rows/cells but they are supposed to
- // be well after the row/cell records. We collect them here
- // see bug 45699
- mergedCellsTable.add((MergeCellsRecord) rec);
- }
- }
- throw new RuntimeException("Failed to find end of row/cell records");
- }
-
private static final class RecordCloner implements RecordVisitor {
private final List _destList;
log.log(POILogger.DEBUG, "Sheet.setDimensions exiting");
}
- /**
- * Set the preoffset when using DBCELL records (currently unused) - this is
- * the position of this sheet within the whole file.
- *
- * @param offset the offset of the sheet's BOF within the file.
- */
-
- public void setPreOffset(int offset)
- {
- this.preoffset = offset;
- }
-
- /**
- * get the preoffset when using DBCELL records (currently unused) - this is
- * the position of this sheet within the whole file.
- *
- * @return offset the offset of the sheet's BOF within the file.
- */
-
- public int getPreOffset()
- {
- return preoffset;
- }
+ public void visitContainedRecords(RecordVisitor rv, int offset) {
- /**
- * Serializes all records in the sheet into one big byte array. Use this to write
- * the sheet out.
- *
- * @param offset to begin write at
- * @param data array containing the binary representation of the records in this sheet
- *
- */
-
- public int serialize(int offset, byte [] data)
- {
- if (log.check( POILogger.DEBUG ))
- log.log(POILogger.DEBUG, "Sheet.serialize using offsets");
-
- int pos = offset;
+ PositionTrackingVisitor ptv = new PositionTrackingVisitor(rv, offset);
+
boolean haveSerializedIndex = false;
for (int k = 0; k < records.size(); k++)
{
RecordBase record = (RecordBase) records.get(k);
- // Don't write out UncalcedRecord entries, as
- // we handle those specially just below
- if (record instanceof UncalcedRecord) {
- continue;
+ if (record instanceof RecordAggregate) {
+ RecordAggregate agg = (RecordAggregate) record;
+ agg.visitContainedRecords(ptv);
+ } else {
+ ptv.visitRecord((Record) record);
}
- // Once the rows have been found in the list of records, start
- // writing out the blocked row information. This includes the DBCell references
- pos += record.serialize(pos, data);
-
// If the BOF record was just serialized then add the IndexRecord
if (record instanceof BOFRecord) {
if (!haveSerializedIndex) {
// If there are diagrams, they have their own BOFRecords,
// and one shouldn't go in after that!
if (_isUncalced) {
- UncalcedRecord rec = new UncalcedRecord();
- pos += rec.serialize(pos, data);
+ ptv.visitRecord(new UncalcedRecord());
}
//Can there be more than one BOF for a sheet? If not then we can
//remove this guard. So be safe it is left here.
if (_rowsAggregate != null) {
- pos += serializeIndexRecord(k, pos, data);
+ // find forward distance to first RowRecord
+ int initRecsSize = getSizeOfInitialSheetRecords(k);
+ int currentPos = ptv.getPosition();
+ ptv.visitRecord(_rowsAggregate.createIndexRecord(currentPos, initRecsSize));
}
}
}
}
- if (log.check( POILogger.DEBUG )) {
- log.log(POILogger.DEBUG, "Sheet.serialize returning ");
- }
- return pos-offset;
}
-
/**
- * @param indexRecordOffset also happens to be the end of the BOF record
- * @return the size of the serialized INDEX record
+ * 'initial sheet records' are between INDEX and the 'Row Blocks'
+ * @param bofRecordIndex index of record after which INDEX record is to be placed
+ * @return count of bytes from end of INDEX record to first ROW record.
*/
- private int serializeIndexRecord(int bofRecordIndex, int indexRecordOffset, byte[] data) {
+ private int getSizeOfInitialSheetRecords(int bofRecordIndex) {
- // 'initial sheet records' are between INDEX and first ROW record.
- int sizeOfInitialSheetRecords = 0;
+ int result = 0;
// start just after BOF record (INDEX is not present in this list)
for (int j = bofRecordIndex + 1; j < records.size(); j++) {
- RecordBase tmpRec = ((RecordBase) records.get(j));
- if (tmpRec instanceof UncalcedRecord) {
- continue;
- }
+ RecordBase tmpRec = (RecordBase) records.get(j);
if (tmpRec instanceof RowRecordsAggregate) {
break;
}
- sizeOfInitialSheetRecords += tmpRec.getRecordSize();
+ result += tmpRec.getRecordSize();
}
if (_isUncalced) {
- sizeOfInitialSheetRecords += UncalcedRecord.getStaticRecordSize();
+ result += UncalcedRecord.getStaticRecordSize();
}
- IndexRecord index = _rowsAggregate.createIndexRecord(indexRecordOffset, sizeOfInitialSheetRecords);
- return index.serialize(indexRecordOffset, data);
+ return result;
}
-
/**
* Create a row record. (does not add it to the records contained in this sheet)
*/
}
}
- /**
- * @return the serialized size of this sheet
- */
- public int getSize() {
- int retval = 0;
-
- for ( int k = 0; k < records.size(); k++) {
- RecordBase record = (RecordBase) records.get(k);
- if (record instanceof UncalcedRecord) {
- // skip the UncalcedRecord if present, it's only encoded if the isUncalced flag is set
- continue;
- }
- retval += record.getRecordSize();
- }
- // add space for IndexRecord if needed
- if (_rowsAggregate != null) {
- // rowsAggregate knows how to make the index record
- retval += IndexRecord.getRecordSizeForBlockCount(_rowsAggregate.getRowBlockCount());
- }
- // Add space for UncalcedRecord
- if (_isUncalced) {
- retval += UncalcedRecord.getStaticRecordSize();
- }
- return retval;
- }
-
public List getRecords()
{
return records;
}
return _dataValidityTable;
}
+
+ public FormulaRecordAggregate createFormula(int row, int col) {
+ return _rowsAggregate.createFormula(row, col);
+ }
}
package org.apache.poi.hssf.record;\r
\r
import org.apache.poi.hssf.record.formula.Ptg;\r
-import org.apache.poi.hssf.util.CellRangeAddress8Bit;\r
import org.apache.poi.util.HexDump;\r
import org.apache.poi.util.LittleEndian;\r
\r
* \r
* @author Josh Micich\r
*/ \r
-public final class ArrayRecord extends Record {\r
+public final class ArrayRecord extends SharedValueRecordBase {\r
\r
public final static short sid = 0x0221;\r
private static final int OPT_ALWAYS_RECALCULATE = 0x0001;\r
private static final int OPT_CALCULATE_ON_OPEN = 0x0002;\r
-\r
- private CellRangeAddress8Bit _range;\r
\r
private int _options;\r
private int _field3notUsed;\r
\r
public ArrayRecord(RecordInputStream in) {\r
super(in);\r
+ _options = in.readUShort();\r
+ _field3notUsed = in.readInt();\r
+ int formulaLen = in.readUShort();\r
+ _formulaTokens = Ptg.readTokens(formulaLen, in);\r
}\r
\r
public boolean isAlwaysRecalculate() {\r
return (_options & OPT_CALCULATE_ON_OPEN) != 0;\r
}\r
\r
- protected void validateSid(short id) {\r
- if (id != sid) {\r
- throw new RecordFormatException("NOT A valid Array RECORD");\r
- }\r
+ protected int getExtraDataSize() {\r
+ return 2 + 4\r
+ + 2 + Ptg.getEncodedSize(_formulaTokens);\r
}\r
-\r
- private int getDataSize(){\r
- return CellRangeAddress8Bit.ENCODED_SIZE \r
- + 2 + 4\r
- + getFormulaSize();\r
- }\r
-\r
- public int serialize( int offset, byte[] data ) {\r
- int dataSize = getDataSize();\r
-\r
- LittleEndian.putShort(data, 0 + offset, sid);\r
- LittleEndian.putUShort(data, 2 + offset, dataSize);\r
-\r
- int pos = offset+4;\r
- _range.serialize(pos, data);\r
- pos += CellRangeAddress8Bit.ENCODED_SIZE;\r
+ protected void serializeExtraData(int offset, byte[] data) {\r
+ int pos = offset;\r
LittleEndian.putUShort(data, pos, _options);\r
pos+=2;\r
LittleEndian.putInt(data, pos, _field3notUsed);\r
LittleEndian.putUShort(data, pos, tokenSize);\r
pos+=2;\r
Ptg.serializePtgs(_formulaTokens, data, pos);\r
- return dataSize + 4;\r
- }\r
-\r
- private int getFormulaSize() {\r
- int result = 0;\r
- for (int i = 0; i < _formulaTokens.length; i++) {\r
- result += _formulaTokens[i].getSize();\r
- }\r
- return result;\r
- }\r
-\r
-\r
- public int getRecordSize(){\r
- return 4 + getDataSize();\r
- }\r
-\r
-\r
- protected void fillFields(RecordInputStream in) {\r
- _range = new CellRangeAddress8Bit(in);\r
- _options = in.readUShort();\r
- _field3notUsed = in.readInt();\r
- int formulaLen = in.readUShort();\r
- _formulaTokens = Ptg.readTokens(formulaLen, in);\r
}\r
\r
public short getSid() {\r
public String toString() {\r
StringBuffer sb = new StringBuffer();\r
sb.append(getClass().getName()).append(" [ARRAY]\n");\r
- sb.append(" range=").append(_range.toString()).append("\n");\r
+ sb.append(" range=").append(getRange().toString()).append("\n");\r
sb.append(" options=").append(HexDump.shortToHex(_options)).append("\n");\r
sb.append(" notUsed=").append(HexDump.intToHex(_field3notUsed)).append("\n");\r
sb.append(" formula:").append("\n");\r
for (int i = 0; i < _formulaTokens.length; i++) {\r
- sb.append(_formulaTokens[i].toString());\r
+ Ptg ptg = _formulaTokens[i];\r
+ sb.append(ptg.toString()).append(ptg.getRVAType()).append("\n");\r
}\r
sb.append("]");\r
return sb.toString();\r
package org.apache.poi.hssf.record;
-import java.util.Arrays;
-import java.util.List;
-
import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.LittleEndian;
/**
- * Formula Record.
+ * Formula Record (0x0006).
* REFERENCE: PG 317/444 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2)<P>
* @author Andrew C. Oliver (acoliver at apache dot org)
* @author Jason Height (jheight at chariot dot net dot au)
for (int k = 0; k < field_8_parsed_expr.length; k++ ) {
sb.append(" Ptg[").append(k).append("]=");
- sb.append(field_8_parsed_expr[k].toString()).append("\n");
+ Ptg ptg = field_8_parsed_expr[k];
+ sb.append(ptg.toString()).append(ptg.getRVAType()).append("\n");
}
sb.append("[/FORMULA]\n");
return sb.toString();
* @author Csaba Nagy (ncsaba at yahoo dot com)
*/
public final class RecordFactory {
- private static final int NUM_RECORDS = 512;
+ private static final int NUM_RECORDS = 512;
private static final Class[] CONSTRUCTOR_ARGS = { RecordInputStream.class, };
* Note - this most but not *every* subclass of Record.
*/
private static final Class[] records = {
+ ArrayRecord.class,
BackupRecord.class,
BlankRecord.class,
BOFRecord.class,
import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.hssf.record.formula.RefNPtg;
import org.apache.poi.hssf.record.formula.RefPtg;
-import org.apache.poi.hssf.util.CellRangeAddress8Bit;
import org.apache.poi.util.HexDump;
/**
* record types.
* @author Danny Mui at apache dot org
*/
-public final class SharedFormulaRecord extends Record {
+public final class SharedFormulaRecord extends SharedValueRecordBase {
public final static short sid = 0x04BC;
- private CellRangeAddress8Bit _range;
private int field_5_reserved;
private Ptg[] field_7_parsed_expr;
public SharedFormulaRecord() {
- _range = new CellRangeAddress8Bit(0, 0, 0, 0);
- field_7_parsed_expr = Ptg.EMPTY_PTG_ARRAY;
+ field_7_parsed_expr = Ptg.EMPTY_PTG_ARRAY;
}
/**
* @param in the RecordInputstream to read the record from
*/
public SharedFormulaRecord(RecordInputStream in) {
- super(in);
- }
-
- protected void validateSid(short id) {
- if (id != this.sid) {
- throw new RecordFormatException("Not a valid SharedFormula");
- }
- }
-
- public int getFirstRow() {
- return _range.getFirstRow();
- }
-
- public int getLastRow() {
- return _range.getLastRow();
- }
-
- public short getFirstColumn() {
- return (short) _range.getFirstColumn();
- }
-
- public short getLastColumn() {
- return (short) _range.getLastColumn();
+ super(in);
+ field_5_reserved = in.readShort();
+ int field_6_expression_len = in.readShort();
+ field_7_parsed_expr = Ptg.readTokens(field_6_expression_len, in);
}
-
- /**
- * spit the record out AS IS. no interpretation or identification
- */
-
- public int serialize(int offset, byte [] data)
- {
+ protected void serializeExtraData(int offset, byte[] data) {
//Because this record is converted to individual Formula records, this method is not required.
throw new UnsupportedOperationException("Cannot serialize a SharedFormulaRecord");
}
-
- public int getRecordSize()
- {
+
+ protected int getExtraDataSize() {
//Because this record is converted to individual Formula records, this method is not required.
throw new UnsupportedOperationException("Cannot get the size for a SharedFormulaRecord");
StringBuffer buffer = new StringBuffer();
buffer.append("[SHARED FORMULA (").append(HexDump.intToHex(sid)).append("]\n");
- buffer.append(" .range = ").append(_range.toString()).append("\n");
+ buffer.append(" .range = ").append(getRange().toString()).append("\n");
buffer.append(" .reserved = ").append(HexDump.shortToHex(field_5_reserved)).append("\n");
for (int k = 0; k < field_7_parsed_expr.length; k++ ) {
buffer.append("Formula[").append(k).append("]");
- buffer.append(field_7_parsed_expr[k].toString()).append("\n");
+ Ptg ptg = field_7_parsed_expr[k];
+ buffer.append(ptg.toString()).append(ptg.getRVAType()).append("\n");
}
buffer.append("[/SHARED FORMULA]\n");
return sid;
}
- protected void fillFields(RecordInputStream in) {
- _range = new CellRangeAddress8Bit(in);
- field_5_reserved = in.readShort();
- int field_6_expression_len = in.readShort();
- field_7_parsed_expr = Ptg.readTokens(field_6_expression_len, in);
- }
-
- /**
- * Are we shared by the supplied formula record?
- */
- public boolean isFormulaInShared(FormulaRecord formula) {
- final int formulaRow = formula.getRow();
- final int formulaColumn = formula.getColumn();
- return ((getFirstRow() <= formulaRow) && (getLastRow() >= formulaRow) &&
- (getFirstColumn() <= formulaColumn) && (getLastColumn() >= formulaColumn));
- }
-
/**
* Creates a non shared formula from the shared formula
* counter part
areaNPtg.isFirstColRelative(),
areaNPtg.isLastColRelative());
} else {
- if (false) {// do we need a ptg clone here?
- ptg = ptg.copy();
- }
+ if (false) {// do we need a ptg clone here?
+ ptg = ptg.copy();
+ }
}
if (!ptg.isBaseToken()) {
ptg.setClass(originalOperandClass);
* counter part
*/
public void convertSharedFormulaRecord(FormulaRecord formula) {
- //Sanity checks
- if (!isFormulaInShared(formula)) {
+ int formulaRow = formula.getRow();
+ int formulaColumn = formula.getColumn();
+ //Sanity checks
+ if (!isInRange(formulaRow, formulaColumn)) {
throw new RuntimeException("Shared Formula Conversion: Coding Error");
}
- final int formulaRow = formula.getRow();
- final int formulaColumn = formula.getColumn();
Ptg[] ptgs = convertSharedFormulas(field_7_parsed_expr, formulaRow, formulaColumn);
formula.setParsedExpression(ptgs);
return row;
}
- /**
- * Mirroring formula records so it is registered in the ValueRecordsAggregate
- */
- public boolean isInValueSection()
- {
- return true;
- }
-
-
- /**
- * Register it in the ValueRecordsAggregate so it can go into the FormulaRecordAggregate
- */
- public boolean isValue() {
- return true;
- }
-
public Object clone() {
//Because this record is converted to individual Formula records, this method is not required.
throw new UnsupportedOperationException("Cannot clone a SharedFormulaRecord");
--- /dev/null
+/* ====================================================================
+ 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.hssf.record;
+
+import org.apache.poi.hssf.util.CellRangeAddress8Bit;
+import org.apache.poi.util.LittleEndian;
+
+/**
+ * Common base class for {@link SharedFormulaRecord}, {@link ArrayRecord} and
+ * {@link TableRecord} which are have similarities.
+ *
+ * @author Josh Micich
+ */
+public abstract class SharedValueRecordBase extends Record {
+
+ private CellRangeAddress8Bit _range;
+
+ protected SharedValueRecordBase(CellRangeAddress8Bit range) {
+ _range = range;
+ }
+
+ protected SharedValueRecordBase() {
+ this(new CellRangeAddress8Bit(0, 0, 0, 0));
+ }
+
+ /**
+ * reads only the range (1 {@link CellRangeAddress8Bit}) from the stream
+ */
+ public SharedValueRecordBase(RecordInputStream in) {
+ _range = new CellRangeAddress8Bit(in);
+ }
+
+ protected final void validateSid(short id) {
+ if (id != getSid()) {
+ throw new RecordFormatException("Not a valid SharedFormula");
+ }
+ }
+
+ public final CellRangeAddress8Bit getRange() {
+ return _range;
+ }
+
+ public final int getFirstRow() {
+ return _range.getFirstRow();
+ }
+
+ public final int getLastRow() {
+ return _range.getLastRow();
+ }
+
+ public final int getFirstColumn() {
+ return (short) _range.getFirstColumn();
+ }
+
+ public final int getLastColumn() {
+ return (short) _range.getLastColumn();
+ }
+
+ public final int getRecordSize() {
+ return 4 + CellRangeAddress8Bit.ENCODED_SIZE + getExtraDataSize();
+ }
+
+ protected abstract int getExtraDataSize();
+
+ protected abstract void serializeExtraData(int offset, byte[] data);
+
+ public final int serialize(int offset, byte[] data) {
+ int dataSize = CellRangeAddress8Bit.ENCODED_SIZE + getExtraDataSize();
+
+ LittleEndian.putShort(data, 0 + offset, getSid());
+ LittleEndian.putUShort(data, 2 + offset, dataSize);
+
+ int pos = offset + 4;
+ _range.serialize(pos, data);
+ pos += CellRangeAddress8Bit.ENCODED_SIZE;
+ serializeExtraData(pos, data);
+ return dataSize + 4;
+ }
+
+ protected final void fillFields(RecordInputStream in) {
+ throw new RuntimeException("Should not be called. Fields are filled in constructor");
+ }
+
+ /**
+ * @return <code>true</code> if (rowIx, colIx) is within the range ({@link #getRange()})
+ * of this shared value object.
+ */
+ public final boolean isInRange(int rowIx, int colIx) {
+ CellRangeAddress8Bit r = _range;
+ return r.getFirstRow() <= rowIx
+ && r.getLastRow() >= rowIx
+ && r.getFirstColumn() <= colIx
+ && r.getLastColumn() >= colIx;
+ }
+ /**
+ * @return <code>true</code> if (rowIx, colIx) describes the first cell in this shared value
+ * object's range ({@link #getRange()})
+ */
+ public final boolean isFirstCell(int rowIx, int colIx) {
+ CellRangeAddress8Bit r = getRange();
+ return r.getFirstRow() == rowIx && r.getFirstColumn() == colIx;
+ }
+
+ /**
+ * Mirroring formula records so it is registered in the
+ * ValueRecordsAggregate
+ */
+ public final boolean isInValueSection() {
+ return true;
+ }
+
+ /**
+ * Register it in the ValueRecordsAggregate so it can go into the
+ * FormulaRecordAggregate
+ */
+ public final boolean isValue() {
+ return true;
+ }
+}
*
* See p536 of the June 08 binary docs
*/
-public final class TableRecord extends Record {
+public final class TableRecord extends SharedValueRecordBase {
public static final short sid = 0x0236;
private static final BitField alwaysCalc = BitFieldFactory.getInstance(0x0001);
- private static final BitField reserved1 = BitFieldFactory.getInstance(0x0002);
+ private static final BitField calcOnOpen = BitFieldFactory.getInstance(0x0002);
private static final BitField rowOrColInpCell = BitFieldFactory.getInstance(0x0004);
private static final BitField oneOrTwoVar = BitFieldFactory.getInstance(0x0008);
private static final BitField rowDeleted = BitFieldFactory.getInstance(0x0010);
private static final BitField colDeleted = BitFieldFactory.getInstance(0x0020);
- private static final BitField reserved2 = BitFieldFactory.getInstance(0x0040);
- private static final BitField reserved3 = BitFieldFactory.getInstance(0x0080);
-
- private CellRangeAddress8Bit _range;
private int field_5_flags;
private int field_6_res;
private int field_9_rowInputCol;
private int field_10_colInputCol;
-
- protected void fillFields(RecordInputStream in) {
- _range = new CellRangeAddress8Bit(in);
+ public TableRecord(RecordInputStream in) {
+ super(in);
field_5_flags = in.readByte();
field_6_res = in.readByte();
field_7_rowInputRow = in.readShort();
field_10_colInputCol = in.readShort();
}
- public TableRecord(RecordInputStream in) {
- super(in);
- }
public TableRecord(CellRangeAddress8Bit range) {
- _range = range;
+ super(range);
field_6_res = 0;
}
- public CellRangeAddress8Bit getRange() {
- return _range;
- }
-
public int getFlags() {
return field_5_flags;
}
public short getSid() {
return sid;
}
-
- public int serialize(int offset, byte[] data) {
- int dataSize = getDataSize();
- LittleEndian.putShort(data, 0 + offset, sid);
- LittleEndian.putUShort(data, 2 + offset, dataSize);
-
- _range.serialize(4 + offset, data);
- LittleEndian.putByte(data, 10 + offset, field_5_flags);
- LittleEndian.putByte(data, 11 + offset, field_6_res);
- LittleEndian.putUShort(data, 12 + offset, field_7_rowInputRow);
- LittleEndian.putUShort(data, 14 + offset, field_8_colInputRow);
- LittleEndian.putUShort(data, 16 + offset, field_9_rowInputCol);
- LittleEndian.putUShort(data, 18 + offset, field_10_colInputCol);
-
- return 4 + dataSize;
- }
- private int getDataSize() {
- return CellRangeAddress8Bit.ENCODED_SIZE
- + 2 // 2 byte fields
- + 8; // 4 short fields
- }
-
- public int getRecordSize() {
- return 4+getDataSize();
+ protected int getExtraDataSize() {
+ return
+ 2 // 2 byte fields
+ + 8; // 4 short fields
}
-
- protected void validateSid(short id) {
- if (id != sid)
- {
- throw new RecordFormatException("NOT A TABLE RECORD");
- }
+ protected void serializeExtraData(int offset, byte[] data) {
+ LittleEndian.putByte(data, 0 + offset, field_5_flags);
+ LittleEndian.putByte(data, 1 + offset, field_6_res);
+ LittleEndian.putUShort(data, 2 + offset, field_7_rowInputRow);
+ LittleEndian.putUShort(data, 4 + offset, field_8_colInputRow);
+ LittleEndian.putUShort(data, 6 + offset, field_9_rowInputCol);
+ LittleEndian.putUShort(data, 8 + offset, field_10_colInputCol);
}
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("[TABLE]\n");
- buffer.append(" .range = ").append(_range.toString()).append("\n");
+ buffer.append(" .range = ").append(getRange().toString()).append("\n");
buffer.append(" .flags = ") .append(HexDump.byteToHex(field_5_flags)).append("\n");
buffer.append(" .alwaysClc= ").append(isAlwaysCalc()).append("\n");
buffer.append(" .reserved = ").append(HexDump.intToHex(field_6_res)).append("\n");
package org.apache.poi.hssf.record.aggregates;
-import org.apache.poi.hssf.model.RecordStream;
import org.apache.poi.hssf.record.CellValueRecordInterface;
import org.apache.poi.hssf.record.FormulaRecord;
-import org.apache.poi.hssf.record.SharedFormulaRecord;
+import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.StringRecord;
-import org.apache.poi.hssf.record.TableRecord;
/**
* The formula record aggregate is used to join together the formula record and it's
public final class FormulaRecordAggregate extends RecordAggregate implements CellValueRecordInterface {
private final FormulaRecord _formulaRecord;
+ private SharedValueManager _sharedValueManager;
/** caches the calculated result of the formula */
private StringRecord _stringRecord;
- private TableRecord _tableRecord;
- public FormulaRecordAggregate(FormulaRecord formulaRecord) {
- _formulaRecord = formulaRecord;
- _stringRecord = null;
- }
- public FormulaRecordAggregate(FormulaRecord formulaRecord, RecordStream rs) {
- _formulaRecord = formulaRecord;
- Class nextClass = rs.peekNextClass();
- if (nextClass == SharedFormulaRecord.class) {
- // For (text) shared formulas, the SharedFormulaRecord comes before the StringRecord.
- // In any case it is OK to skip SharedFormulaRecords because they were collected
- // before constructing the ValueRecordsAggregate.
- rs.getNext(); // skip the shared formula record
- nextClass = rs.peekNextClass();
+ /**
+ * @param stringRec may be <code>null</code> if this formula does not have a cached text
+ * value.
+ * @param svm the {@link SharedValueManager} for the current sheet
+ */
+ public FormulaRecordAggregate(FormulaRecord formulaRec, StringRecord stringRec, SharedValueManager svm) {
+ if (svm == null) {
+ throw new IllegalArgumentException("sfm must not be null");
}
- if (nextClass == StringRecord.class) {
- _stringRecord = (StringRecord) rs.getNext();
- } else if (nextClass == TableRecord.class) {
- _tableRecord = (TableRecord) rs.getNext();
+ if (formulaRec.isSharedFormula()) {
+ svm.convertSharedFormulaRecord(formulaRec);
}
+ _formulaRecord = formulaRec;
+ _sharedValueManager = svm;
+ _stringRecord = stringRec;
}
public void setStringRecord(StringRecord stringRecord) {
_stringRecord = stringRecord;
- _tableRecord = null; // probably can't have both present at the same time
- // TODO - establish rules governing when each of these sub records may exist
}
public FormulaRecord getFormulaRecord() {
public void visitContainedRecords(RecordVisitor rv) {
rv.visitRecord(_formulaRecord);
+ Record sharedFormulaRecord = _sharedValueManager.getRecordForFirstCell(_formulaRecord);
+ if (sharedFormulaRecord != null) {
+ rv.visitRecord(sharedFormulaRecord);
+ }
if (_stringRecord != null) {
rv.visitRecord(_stringRecord);
}
- if (_tableRecord != null) {
- rv.visitRecord(_tableRecord);
- }
}
public String getStringValue() {
rv.visitRecord(new MergeCellsRecord(cras, startIx, nLeftoverMergedRegions));
}
}
+ public void addRecords(MergeCellsRecord[] mcrs) {
+ for (int i = 0; i < mcrs.length; i++) {
+ addMergeCellsRecord(mcrs[i]);
+ }
+ }
- public void add(MergeCellsRecord mcr) {
+ private void addMergeCellsRecord(MergeCellsRecord mcr) {
int nRegions = mcr.getNumAreas();
for (int i = 0; i < nRegions; i++) {
_mergedRegions.add(mcr.getAreaAt(i));
public int getNumberOfMergedRegions() {
return _mergedRegions.size();
}
+
}
protected final void fillFields(RecordInputStream in) {\r
throw new RuntimeException("Should not be called");\r
}\r
- public final short getSid() {\r
+ public final short getSid() {\r
throw new RuntimeException("Should not be called");\r
- }\r
+ }\r
\r
+ /**\r
+ * Visit each of the atomic BIFF records contained in this {@link RecordAggregate} in the order\r
+ * that they should be written to file. Implementors may or may not return the actual \r
+ * {@link Record}s being used to manage POI's internal implementation. Callers should not\r
+ * assume either way, and therefore only attempt to modify those {@link Record}s after cloning\r
+ */\r
public abstract void visitContainedRecords(RecordVisitor rv);\r
\r
public final int serialize(int offset, byte[] data) {\r
_totalSize += r.getRecordSize();\r
}\r
}\r
+ /**\r
+ * A wrapper for {@link RecordVisitor} which accumulates the sizes of all\r
+ * records visited.\r
+ */\r
+ public static final class PositionTrackingVisitor implements RecordVisitor {\r
+ private final RecordVisitor _rv;\r
+ private int _position;\r
+\r
+ public PositionTrackingVisitor(RecordVisitor rv, int initialPosition) {\r
+ _rv = rv;\r
+ _position = initialPosition;\r
+ }\r
+ public void visitRecord(Record r) {\r
+ _position += r.getRecordSize();\r
+ _rv.visitRecord(r);\r
+ }\r
+ public void setPosition(int position) {\r
+ _position = position;\r
+ }\r
+ public int getPosition() {\r
+ return _position;\r
+ }\r
+ }\r
}\r
import java.util.Map;
import java.util.TreeMap;
+import org.apache.poi.hssf.model.RecordStream;
+import org.apache.poi.hssf.record.ArrayRecord;
import org.apache.poi.hssf.record.CellValueRecordInterface;
import org.apache.poi.hssf.record.DBCellRecord;
+import org.apache.poi.hssf.record.FormulaRecord;
import org.apache.poi.hssf.record.IndexRecord;
import org.apache.poi.hssf.record.MergeCellsRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.RowRecord;
+import org.apache.poi.hssf.record.SharedFormulaRecord;
+import org.apache.poi.hssf.record.TableRecord;
import org.apache.poi.hssf.record.UnknownRecord;
/**
private final Map _rowRecords;
private final ValueRecordsAggregate _valuesAgg;
private final List _unknownRecords;
+ private final SharedValueManager _sharedValueManager;
/** Creates a new instance of ValueRecordsAggregate */
-
public RowRecordsAggregate() {
- this(new TreeMap(), new ValueRecordsAggregate());
+ this(SharedValueManager.EMPTY);
}
- private RowRecordsAggregate(TreeMap rowRecords, ValueRecordsAggregate valuesAgg) {
- _rowRecords = rowRecords;
- _valuesAgg = valuesAgg;
+ private RowRecordsAggregate(SharedValueManager svm) {
+ _rowRecords = new TreeMap();
+ _valuesAgg = new ValueRecordsAggregate();
_unknownRecords = new ArrayList();
+ _sharedValueManager = svm;
}
- public RowRecordsAggregate(List recs, int startIx, int endIx) {
- this();
- // First up, locate all the shared formulas for this sheet
- SharedFormulaHolder sfh = SharedFormulaHolder.create(recs, startIx, endIx);
- for(int i=startIx; i<endIx; i++) {
- Record rec = (Record) recs.get(i);
+ /**
+ * @param rs record stream with all {@link SharedFormulaRecord}
+ * {@link ArrayRecord}, {@link TableRecord} {@link MergeCellsRecord} Records removed
+ */
+ public RowRecordsAggregate(RecordStream rs, SharedValueManager svm) {
+ this(svm);
+ while(rs.hasNext()) {
+ Record rec = rs.getNext();
switch (rec.getSid()) {
- case MergeCellsRecord.sid:
- // Some apps scatter these records between the rows/cells but they are supposed to
- // be well after the row/cell records. It is assumed such rogue MergeCellRecords
- // have already been collected by the caller, and can safely be ignored here.
- // see bug 45699
- continue;
case RowRecord.sid:
insertRow((RowRecord) rec);
continue;
case DBCellRecord.sid:
// end of 'Row Block'. Should only occur after cell records
+ // ignore DBCELL records because POI generates them upon re-serialization
continue;
}
if (rec instanceof UnknownRecord) {
if (!rec.isValue()) {
throw new RuntimeException("Unexpected record type (" + rec.getClass().getName() + ")");
}
- i += _valuesAgg.construct(recs, i, endIx, sfh)-1;
+ _valuesAgg.construct((CellValueRecordInterface)rec, rs, svm);
}
- "".length();
}
/**
* Handles UnknownRecords which appear within the row/cell records
// 0x01C2 // several
// 0x0034 // few
// No documentation could be found for these
-
+
// keep the unknown records for re-serialization
_unknownRecords.add(rec);
}
{
return _lastrow;
}
-
+
/** Returns the number of row blocks.
* <p/>The row blocks are goupings of rows that contain the DBCell record
* after them
}
return row.getRowNumber();
}
-
+
private int visitRowRecordsForBlock(int blockIndex, RecordVisitor rv) {
final int startIndex = blockIndex*DBCellRecord.BLOCK_SIZE;
final int endIndex = startIndex + DBCellRecord.BLOCK_SIZE;
rv.visitRecord(rec);
}
return result;
- }
-
+ }
+
public void visitContainedRecords(RecordVisitor rv) {
- ValueRecordsAggregate cells = _valuesAgg;
-
+
+ PositionTrackingVisitor stv = new PositionTrackingVisitor(rv, 0);
//DBCells are serialized before row records.
final int blockCount = getRowBlockCount();
for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) {
// Note: Cell references start from the second row...
int cellRefOffset = (rowBlockSize - RowRecord.ENCODED_SIZE);
for (int row = startRowNumber; row <= endRowNumber; row++) {
- if (cells.rowHasCells(row)) {
- final int rowCellSize = cells.visitCellsForRow(row, rv);
+ if (_valuesAgg.rowHasCells(row)) {
+ stv.setPosition(0);
+ _valuesAgg.visitCellsForRow(row, stv);
+ int rowCellSize = stv.getPosition();
pos += rowCellSize;
// Add the offset to the first cell for the row into the
// DBCellRecord.
public Iterator getIterator() {
return _rowRecords.values().iterator();
}
-
-
+
public Iterator getAllRecordsIterator() {
List result = new ArrayList(_rowRecords.size() * 2);
result.addAll(_rowRecords.values());
public void removeCell(CellValueRecordInterface cvRec) {
_valuesAgg.removeCell(cvRec);
}
+ public FormulaRecordAggregate createFormula(int row, int col) {
+ FormulaRecord fr = new FormulaRecord();
+ fr.setRow(row);
+ fr.setColumn((short) col);
+ return new FormulaRecordAggregate(fr, null, _sharedValueManager);
+ }
}
-
+++ /dev/null
-/* ====================================================================
- 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.hssf.record.aggregates;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.poi.hssf.record.FormulaRecord;
-import org.apache.poi.hssf.record.Record;
-import org.apache.poi.hssf.record.SharedFormulaRecord;
-
-/**
- * Temporarily holds SharedFormulaRecords while constructing a <tt>RowRecordsAggregate</tt>
- *
- * @author Josh Micich
- */
-final class SharedFormulaHolder {
-
- private static final SharedFormulaHolder EMPTY = new SharedFormulaHolder(new SharedFormulaRecord[0]);
- private final SharedFormulaRecord[] _sfrs;
-
- /**
- * @param recs list of sheet records (possibly contains records for other parts of the Excel file)
- * @param startIx index of first row/cell record for current sheet
- * @param endIx one past index of last row/cell record for current sheet. It is important
- * that this code does not inadvertently collect <tt>SharedFormulaRecord</tt>s from any other
- * sheet (which could happen if endIx is chosen poorly). (see bug 44449)
- */
- public static SharedFormulaHolder create(List recs, int startIx, int endIx) {
- List temp = new ArrayList();
- for (int k = startIx; k < endIx; k++)
- {
- Record rec = ( Record ) recs.get(k);
- if (rec instanceof SharedFormulaRecord) {
- temp.add(rec);
- }
- }
- if (temp.size() < 1) {
- return EMPTY;
- }
- SharedFormulaRecord[] sfrs = new SharedFormulaRecord[temp.size()];
- temp.toArray(sfrs);
- return new SharedFormulaHolder(sfrs);
-
- }
- private SharedFormulaHolder(SharedFormulaRecord[] sfrs) {
- _sfrs = sfrs;
- }
- public void convertSharedFormulaRecord(FormulaRecord formula) {
- // Traverse the list of shared formulas in
- // reverse order, and try to find the correct one
- // for us
- for (int i=0; i<_sfrs.length; i++) {
- SharedFormulaRecord shrd = _sfrs[i];
- if (shrd.isFormulaInShared(formula)) {
- shrd.convertSharedFormulaRecord(formula);
- return;
- }
- }
- // not found
- handleMissingSharedFormulaRecord(formula);
- }
-
- /**
- * Sometimes the shared formula flag "seems" to be erroneously set, in which case there is no
- * call to <tt>SharedFormulaRecord.convertSharedFormulaRecord</tt> and hence the
- * <tt>parsedExpression</tt> field of this <tt>FormulaRecord</tt> will not get updated.<br/>
- * As it turns out, this is not a problem, because in these circumstances, the existing value
- * for <tt>parsedExpression</tt> is perfectly OK.<p/>
- *
- * This method may also be used for setting breakpoints to help diagnose issues regarding the
- * abnormally-set 'shared formula' flags.
- * (see TestValueRecordsAggregate.testSpuriousSharedFormulaFlag()).<p/>
- *
- * The method currently does nothing but do not delete it without finding a nice home for this
- * comment.
- */
- private static void handleMissingSharedFormulaRecord(FormulaRecord formula) {
- // could log an info message here since this is a fairly unusual occurrence.
- }
-}
--- /dev/null
+/* ====================================================================
+ 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.hssf.record.aggregates;
+
+import org.apache.poi.hssf.record.ArrayRecord;
+import org.apache.poi.hssf.record.FormulaRecord;
+import org.apache.poi.hssf.record.SharedFormulaRecord;
+import org.apache.poi.hssf.record.SharedValueRecordBase;
+import org.apache.poi.hssf.record.TableRecord;
+
+/**
+ * Manages various auxiliary records while constructing a
+ * {@link RowRecordsAggregate}:
+ * <ul>
+ * <li>{@link SharedFormulaRecord}s</li>
+ * <li>{@link ArrayRecord}s</li>
+ * <li>{@link TableRecord}s</li>
+ * </ul>
+ *
+ * @author Josh Micich
+ */
+public final class SharedValueManager {
+
+ public static final SharedValueManager EMPTY = new SharedValueManager(
+ new SharedFormulaRecord[0], new ArrayRecord[0], new TableRecord[0]);
+ private final SharedFormulaRecord[] _sfrs;
+ private final ArrayRecord[] _arrayRecords;
+ private final TableRecord[] _tableRecords;
+
+ private SharedValueManager(SharedFormulaRecord[] sharedFormulaRecords,
+ ArrayRecord[] arrayRecords, TableRecord[] tableRecords) {
+ _sfrs = sharedFormulaRecords;
+ _arrayRecords = arrayRecords;
+ _tableRecords = tableRecords;
+ }
+
+ /**
+ * @param recs list of sheet records (possibly contains records for other parts of the Excel file)
+ * @param startIx index of first row/cell record for current sheet
+ * @param endIx one past index of last row/cell record for current sheet. It is important
+ * that this code does not inadvertently collect <tt>SharedFormulaRecord</tt>s from any other
+ * sheet (which could happen if endIx is chosen poorly). (see bug 44449)
+ */
+ public static SharedValueManager create(SharedFormulaRecord[] sharedFormulaRecords,
+ ArrayRecord[] arrayRecords, TableRecord[] tableRecords) {
+ if (sharedFormulaRecords.length + arrayRecords.length + tableRecords.length < 1) {
+ return EMPTY;
+ }
+ return new SharedValueManager(sharedFormulaRecords, arrayRecords, tableRecords);
+ }
+
+ public void convertSharedFormulaRecord(FormulaRecord formula) {
+ int row = formula.getRow();
+ int column = formula.getColumn();
+ // Traverse the list of shared formulas in
+ // reverse order, and try to find the correct one
+ // for us
+ for (int i = 0; i < _sfrs.length; i++) {
+ SharedFormulaRecord shrd = _sfrs[i];
+ if (shrd.isInRange(row, column)) {
+ shrd.convertSharedFormulaRecord(formula);
+ return;
+ }
+ }
+ // not found
+ handleMissingSharedFormulaRecord(formula);
+ }
+
+ /**
+ * Sometimes the shared formula flag "seems" to be erroneously set, in which case there is no
+ * call to <tt>SharedFormulaRecord.convertSharedFormulaRecord</tt> and hence the
+ * <tt>parsedExpression</tt> field of this <tt>FormulaRecord</tt> will not get updated.<br/>
+ * As it turns out, this is not a problem, because in these circumstances, the existing value
+ * for <tt>parsedExpression</tt> is perfectly OK.<p/>
+ *
+ * This method may also be used for setting breakpoints to help diagnose issues regarding the
+ * abnormally-set 'shared formula' flags.
+ * (see TestValueRecordsAggregate.testSpuriousSharedFormulaFlag()).<p/>
+ *
+ * The method currently does nothing but do not delete it without finding a nice home for this
+ * comment.
+ */
+ private static void handleMissingSharedFormulaRecord(FormulaRecord formula) {
+ // could log an info message here since this is a fairly unusual occurrence.
+ formula.setSharedFormula(false); // no point leaving the flag erroneously set
+ }
+
+ /**
+ * Note - does not return SharedFormulaRecords currently, because the corresponding formula
+ * records have been converted to 'unshared'. POI does not attempt to re-share formulas. On
+ * the other hand, there is no such conversion for array or table formulas, so this method
+ * returns the TABLE or ARRAY record (if it should be written after the specified
+ * formulaRecord.
+ *
+ * @return the TABLE or ARRAY record for this formula cell, if it is the first cell of a
+ * table or array region.
+ */
+ public SharedValueRecordBase getRecordForFirstCell(FormulaRecord formulaRecord) {
+ int row = formulaRecord.getRow();
+ int column = formulaRecord.getColumn();
+ for (int i = 0; i < _tableRecords.length; i++) {
+ TableRecord tr = _tableRecords[i];
+ if (tr.isFirstCell(row, column)) {
+ return tr;
+ }
+ }
+ for (int i = 0; i < _arrayRecords.length; i++) {
+ ArrayRecord ar = _arrayRecords[i];
+ if (ar.isFirstCell(row, column)) {
+ return ar;
+ }
+ }
+ return null;
+ }
+}
import org.apache.poi.hssf.model.RecordStream;
import org.apache.poi.hssf.record.CellValueRecordInterface;
-import org.apache.poi.hssf.record.DBCellRecord;
import org.apache.poi.hssf.record.FormulaRecord;
-import org.apache.poi.hssf.record.MergeCellsRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.RecordBase;
-import org.apache.poi.hssf.record.RowRecord;
-import org.apache.poi.hssf.record.SharedFormulaRecord;
import org.apache.poi.hssf.record.StringRecord;
-import org.apache.poi.hssf.record.TableRecord;
-import org.apache.poi.hssf.record.UnknownRecord;
import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
/**
}
/**
- * Processes a sequential group of cell value records. Stops at endIx or the first
- * non-value record encountered.
- * @param sfh used to resolve any shared formulas for the current sheet
- * @return the number of records consumed
+ * Processes a single cell value record
+ * @param sfh used to resolve any shared-formulas/arrays/tables for the current sheet
*/
- public int construct(List records, int offset, int endIx, SharedFormulaHolder sfh) {
- RecordStream rs = new RecordStream(records, offset, endIx);
-
- // Now do the main processing sweep
- while (rs.hasNext()) {
- Class recClass = rs.peekNextClass();
- if (recClass == StringRecord.class) {
- throw new RuntimeException("Loose StringRecord found without preceding FormulaRecord");
+ public void construct(CellValueRecordInterface rec, RecordStream rs, SharedValueManager sfh) {
+ if (rec instanceof FormulaRecord) {
+ FormulaRecord formulaRec = (FormulaRecord)rec;
+ if (formulaRec.isSharedFormula()) {
+ sfh.convertSharedFormulaRecord(formulaRec);
}
-
- if (recClass == TableRecord.class) {
- throw new RuntimeException("Loose TableRecord found without preceding FormulaRecord");
- }
-
- if (recClass == UnknownRecord.class) {
- break;
- }
- if (recClass == RowRecord.class) {
- break;
- }
- if (recClass == DBCellRecord.class) {
- // end of 'Row Block'. This record is ignored by POI
- break;
+ // read optional cached text value
+ StringRecord cachedText;
+ Class nextClass = rs.peekNextClass();
+ if (nextClass == StringRecord.class) {
+ cachedText = (StringRecord) rs.getNext();
+ } else {
+ cachedText = null;
}
-
- Record rec = rs.getNext();
-
- if (recClass == SharedFormulaRecord.class) {
- // Already handled, not to worry
- continue;
- }
- if (recClass == MergeCellsRecord.class) {
- // doesn't really belong here
- // can safely be ignored, because it has been processed in a higher method
- continue;
- }
-
- if (!rec.isValue()) {
- throw new RuntimeException("bad record type");
- }
- if (rec instanceof FormulaRecord) {
- FormulaRecord formula = (FormulaRecord)rec;
- if (formula.isSharedFormula()) {
- sfh.convertSharedFormulaRecord(formula);
- }
-
- insertCell(new FormulaRecordAggregate((FormulaRecord)rec, rs));
- continue;
- }
- insertCell(( CellValueRecordInterface ) rec);
+ insertCell(new FormulaRecordAggregate(formulaRec, cachedText, sfh));
+ } else {
+ insertCell(rec);
}
- return rs.getCountRead();
}
/** Tallies a count of the size of the cell records
return pos - offset;
}
- public int visitCellsForRow(int rowIndex, RecordVisitor rv) {
- int result = 0;
+ public void visitCellsForRow(int rowIndex, RecordVisitor rv) {
+
CellValueRecordInterface[] cellRecs = records[rowIndex];
if (cellRecs != null) {
for (int i = 0; i < cellRecs.length; i++) {
if (cvr == null) {
continue;
}
- if (cvr instanceof FormulaRecordAggregate) {
- FormulaRecordAggregate fmAgg = (FormulaRecordAggregate) cvr;
- Record fmAggRec = fmAgg.getFormulaRecord();
- rv.visitRecord(fmAggRec);
- result += fmAggRec.getRecordSize();
- fmAggRec = fmAgg.getStringRecord();
- if (fmAggRec != null) {
- rv.visitRecord(fmAggRec);
- result += fmAggRec.getRecordSize();
- }
+ if (cvr instanceof RecordAggregate) {
+ RecordAggregate agg = (RecordAggregate) cvr;
+ agg.visitContainedRecords(rv);
} else {
Record rec = (Record) cvr;
rv.visitRecord(rec);
- result += rec.getRecordSize();
}
}
}
- return result;
}
public CellValueRecordInterface[] getValueRecords() {
{
case CELL_TYPE_FORMULA :
- FormulaRecordAggregate frec = null;
-
- if (cellType != this.cellType)
- {
- frec = new FormulaRecordAggregate(new FormulaRecord());
- }
- else
- {
- frec = ( FormulaRecordAggregate ) record;
+ FormulaRecordAggregate frec;
+
+ if (cellType != this.cellType) {
+ frec = sheet.createFormula(row, col);
+ } else {
+ frec = (FormulaRecordAggregate) record;
+ frec.setRow(row);
+ frec.setColumn(col);
}
- frec.setColumn(col);
if (setValue)
{
frec.getFormulaRecord().setValue(getNumericCellValue());
}
frec.setXFIndex(styleIndex);
- frec.setRow(row);
record = frec;
break;
import org.apache.poi.hssf.record.SSTRecord;
import org.apache.poi.hssf.record.UnicodeString;
import org.apache.poi.hssf.record.UnknownRecord;
+import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
import org.apache.poi.hssf.record.formula.Area3DPtg;
import org.apache.poi.hssf.record.formula.MemFuncPtg;
import org.apache.poi.hssf.record.formula.NameXPtg;
*/
private ArrayList names;
-
+
/**
* this holds the HSSFFont objects attached to this workbook.
* We only create these from the low level records as required.
* someplace else.
*/
private HSSFDataFormat formatter;
-
+
/**
* The policy to apply in the event of missing or
* blank cells when fetching from a row.
/**
* Sets the policy on what to do when
* getting missing or blank cells from a row.
- * This will then apply to all calls to
+ * This will then apply to all calls to
* {@link HSSFRow.getCell()}. See
* {@link MissingCellPolicy}
*/
private void validateSheetIndex(int index) {
int lastSheetIx = _sheets.size() - 1;
if (index < 0 || index > lastSheetIx) {
- throw new IllegalArgumentException("Sheet index ("
+ throw new IllegalArgumentException("Sheet index ("
+ index +") is out of range (0.." + lastSheetIx + ")");
}
}
-
+
/**
* Selects a single sheet. This may be different to
- * the 'active' sheet (which is the sheet with focus).
+ * the 'active' sheet (which is the sheet with focus).
*/
public void setSelectedTab(int index) {
-
+
validateSheetIndex(index);
int nSheets = _sheets.size();
for (int i=0; i<nSheets; i++) {
setSelectedTab((int)index);
}
public void setSelectedTabs(int[] indexes) {
-
+
for (int i = 0; i < indexes.length; i++) {
validateSheetIndex(indexes[i]);
}
bSelect = true;
break;
}
-
+
}
getSheetAt(i).setSelected(bSelect);
}
* 'Selected' sheet(s) is a distinct concept.
*/
public void setActiveSheet(int index) {
-
+
validateSheetIndex(index);
int nSheets = _sheets.size();
for (int i=0; i<nSheets; i++) {
}
/**
* deprecated May 2008
- * @deprecated - Misleading name - use getActiveSheetIndex()
+ * @deprecated - Misleading name - use getActiveSheetIndex()
*/
public short getSelectedTab() {
return (short) getActiveSheetIndex();
}
-
+
/**
* sets the first tab that is displayed in the list of tabs
* in excel.
}
/**
* deprecated May 2008
- * @deprecated - Misleading name - use setFirstVisibleTab()
+ * @deprecated - Misleading name - use setFirstVisibleTab()
*/
public void setDisplayedTab(short index) {
setFirstVisibleTab(index);
}
/**
* deprecated May 2008
- * @deprecated - Misleading name - use getFirstVisibleTab()
+ * @deprecated - Misleading name - use getFirstVisibleTab()
*/
public short getDisplayedTab() {
return (short) getFirstVisibleTab();
/**
* create an HSSFSheet for this HSSFWorkbook, adds it to the sheets and
* returns the high level representation. Use this to create new sheets.
- *
+ *
* @param sheetname
* sheetname to set for the sheet.
* @return HSSFSheet representing the new sheet.
/**
* Removes sheet at the given index.<p/>
- *
- * Care must be taken if the removed sheet is the currently active or only selected sheet in
- * the workbook. There are a few situations when Excel must have a selection and/or active
+ *
+ * Care must be taken if the removed sheet is the currently active or only selected sheet in
+ * the workbook. There are a few situations when Excel must have a selection and/or active
* sheet. (For example when printing - see Bug 40414).<br/>
- *
+ *
* This method makes sure that if the removed sheet was active, another sheet will become
* active in its place. Furthermore, if the removed sheet was the only selected sheet, another
- * sheet will become selected. The newly active/selected sheet will have the same index, or
+ * sheet will become selected. The newly active/selected sheet will have the same index, or
* one less if the removed sheet was the last in the workbook.
- *
+ *
* @param index of the sheet (0-based)
*/
public void removeSheetAt(int index) {
if(fontindex == Short.MAX_VALUE){
throw new IllegalArgumentException("Maximum number of fonts was exceeded");
}
-
+
// Ask getFontAt() to build it for us,
// so it gets properly cached
return getFontAt(fontindex);
for (short i=0; i<=getNumberOfFonts(); i++) {
// Remember - there is no 4!
if(i == 4) continue;
-
+
HSSFFont hssfFont = getFontAt(i);
if (hssfFont.getBoldweight() == boldWeight
&& hssfFont.getColor() == color
return retval;
}
-
+
/**
* Reset the fonts cache, causing all new calls
* to getFontAt() to create new objects.
//poifs.writeFilesystem(stream);
}
+ /**
+ * Totals the sizes of all sheet records and eventually serializes them
+ */
+ private static final class SheetRecordCollector implements RecordVisitor {
+
+ private List _list;
+ private int _totalSize;
+
+ public SheetRecordCollector() {
+ _totalSize = 0;
+ _list = new ArrayList(128);
+ }
+ public int getTotalSize() {
+ return _totalSize;
+ }
+ public void visitRecord(Record r) {
+ _list.add(r);
+ _totalSize+=r.getRecordSize();
+ }
+ public int serialize(int offset, byte[] data) {
+ int result = 0;
+ int nRecs = _list.size();
+ for(int i=0; i<nRecs; i++) {
+ Record rec = (Record)_list.get(i);
+ result += rec.serialize(offset + result, data);
+ }
+ return result;
+ }
+ }
+
+
/**
* Method getBytes - get the bytes of just the HSSF portions of the XLS file.
* Use this to construct a POI POIFSFileSystem yourself.
* @see org.apache.poi.hssf.model.Workbook
* @see org.apache.poi.hssf.model.Sheet
*/
-
- public byte[] getBytes()
- {
+ public byte[] getBytes() {
if (log.check( POILogger.DEBUG )) {
log.log(DEBUG, "HSSFWorkbook.getBytes()");
}
-
+
HSSFSheet[] sheets = getSheets();
int nSheets = sheets.length;
int totalsize = workbook.getSize();
// pre-calculate all the sheet sizes and set BOF indexes
- int[] estimatedSheetSizes = new int[nSheets];
+ SheetRecordCollector[] srCollectors = new SheetRecordCollector[nSheets];
for (int k = 0; k < nSheets; k++) {
workbook.setSheetBof(k, totalsize);
- int sheetSize = sheets[k].getSheet().getSize();
- estimatedSheetSizes[k] = sheetSize;
- totalsize += sheetSize;
+ SheetRecordCollector src = new SheetRecordCollector();
+ sheets[k].getSheet().visitContainedRecords(src, totalsize);
+ totalsize += src.getTotalSize();
+ srCollectors[k] = src;
}
-
byte[] retval = new byte[totalsize];
int pos = workbook.serialize(0, retval);
for (int k = 0; k < nSheets; k++) {
- int serializedSize = sheets[k].getSheet().serialize(pos, retval);
- if (serializedSize != estimatedSheetSizes[k]) {
+ SheetRecordCollector src = srCollectors[k];
+ int serializedSize = src.serialize(pos, retval);
+ if (serializedSize != src.getTotalSize()) {
// Wrong offset values have been passed in the call to setSheetBof() above.
- // For books with more than one sheet, this discrepancy would cause excel
+ // For books with more than one sheet, this discrepancy would cause excel
// to report errors and loose data while reading the workbook
- throw new IllegalStateException("Actual serialized sheet size (" + serializedSize
- + ") differs from pre-calculated size (" + estimatedSheetSizes[k]
+ throw new IllegalStateException("Actual serialized sheet size (" + serializedSize
+ + ") differs from pre-calculated size (" + src.getTotalSize()
+ ") for sheet (" + k + ")");
// TODO - add similar sanity check to ensure that Sheet.serializeIndexRecord() does not write mis-aligned offsets either
}
}
/**
- * Note - This method should only used by POI internally.
+ * Note - This method should only used by POI internally.
* It may get deleted or change definition in future POI versions
*/
- public NameXPtg getNameXPtg(String name) {
- return workbook.getNameXPtg(name);
- }
+ public NameXPtg getNameXPtg(String name) {
+ return workbook.getNameXPtg(name);
+ }
}
throw new ArrayIndexOutOfBoundsException("Illegal offset");
}
if ((len < 0) || (((string.length - offset) / 2) < len)) {
- throw new IllegalArgumentException("Illegal length");
+ throw new IllegalArgumentException("Illegal length " + len);
}
try {
import java.io.IOException;
import java.io.InputStream;
-import org.apache.poi.ddf.DefaultEscherRecordFactory;
-import org.apache.poi.ddf.EscherRecord;
import org.apache.poi.hpbf.HPBFDocument;
import org.apache.poi.hpbf.model.QuillContents;
import org.apache.poi.hpbf.model.qcbits.QCBit;
-import org.apache.poi.poifs.filesystem.DirectoryNode;
-import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.HexDump;
-import org.apache.poi.util.LittleEndian;
-import org.apache.poi.util.StringUtil;
/**
* For dumping out the PLC contents of QC Bits of a
private HPBFDocument doc;
private QuillContents qc;
- public PLCDumper(HPBFDocument doc) {
- this.doc = doc;
+ public PLCDumper(HPBFDocument hpbfDoc) {
+ doc = hpbfDoc;
qc = doc.getQuillContents();
}
public PLCDumper(POIFSFileSystem fs) throws IOException {
}
private void dumpPLC() {
- QuillContents qc = doc.getQuillContents();
QCBit[] bits = qc.getBits();
for(int i=0; i<bits.length; i++) {
System.out.println("");
System.out.println("Dumping " + bit.getBitType() + " bit at " + index);
System.out.println(" Is a " + bit.getThingType() + ", number is " + bit.getOptA());
- System.out.println(" Starts at " + bit.getDataOffset() + " (" + Integer.toHexString(bit.getDataOffset()) + ")");
- System.out.println(" Runs for " + bit.getLength() + " (" + Integer.toHexString(bit.getLength()) + ")");
+ System.out.println(" Starts at " + bit.getDataOffset() + " (0x" + Integer.toHexString(bit.getDataOffset()) + ")");
+ System.out.println(" Runs for " + bit.getLength() + " (0x" + Integer.toHexString(bit.getLength()) + ")");
System.out.println(HexDump.dump(bit.getData(), 0, 0));
}
import org.apache.poi.hpbf.HPBFDocument;
import org.apache.poi.hpbf.model.qcbits.QCBit;
import org.apache.poi.hpbf.model.qcbits.QCTextBit;
+import org.apache.poi.hpbf.model.qcbits.QCPLCBit.Type12;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
/**
*/
public class PublisherTextExtractor extends POIOLE2TextExtractor {
private HPBFDocument doc;
+ private boolean hyperlinksByDefault = false;
public PublisherTextExtractor(HPBFDocument doc) {
super(doc);
this(new POIFSFileSystem(is));
}
+ /**
+ * Should a call to getText() return hyperlinks inline
+ * with the text?
+ * Default is no
+ */
+ public void setHyperlinksByDefault(boolean hyperlinksByDefault) {
+ this.hyperlinksByDefault = hyperlinksByDefault;
+ }
+
+
public String getText() {
StringBuffer text = new StringBuffer();
}
}
+ // If requested, add in the hyperlinks
+ // Ideally, we'd do these inline, but the hyperlink
+ // positions are relative to the text area the
+ // hyperlink is in, and we have yet to figure out
+ // how to tie that together.
+ if(hyperlinksByDefault) {
+ for(int i=0; i<bits.length; i++) {
+ if(bits[i] != null && bits[i] instanceof Type12) {
+ Type12 hyperlinks = (Type12)bits[i];
+ for(int j=0; j<hyperlinks.getNumberOfHyperlinks(); j++) {
+ text.append("<");
+ text.append(hyperlinks.getHyperlink(j));
+ text.append(">\n");
+ }
+ }
+ }
+ }
+
// Get more text
// TODO
import java.io.IOException;
import org.apache.poi.hpbf.model.qcbits.QCBit;
+import org.apache.poi.hpbf.model.qcbits.QCPLCBit;
import org.apache.poi.hpbf.model.qcbits.QCTextBit;
import org.apache.poi.hpbf.model.qcbits.UnknownQCBit;
import org.apache.poi.poifs.filesystem.DirectoryNode;
// Create
if(bitType.equals("TEXT")) {
bits[i] = new QCTextBit(thingType, bitType, bitData);
+ } else if(bitType.equals("PLC ")) {
+ bits[i] = QCPLCBit.createQCPLCBit(thingType, bitType, bitData);
} else {
bits[i] = new UnknownQCBit(thingType, bitType, bitData);
}
--- /dev/null
+/* ====================================================================
+ 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.hpbf.model.qcbits;
+
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.StringUtil;
+
+
+/**
+ * A "PLC " (PLC) based bit of Quill Contents. The exact
+ * format is determined by the type of the PLCs.
+ */
+public class QCPLCBit extends QCBit {
+ protected int numberOfPLCs;
+ protected int typeOfPLCS;
+ /**
+ * The data which goes before the main PLC entries.
+ * This is apparently always made up of 2 byte
+ * un-signed ints..
+ */
+ protected int[] preData;
+ /** The first value of each PLC, normally 4 bytes */
+ protected long[] plcValA;
+ /** The second value of each PLC, normally 4 bytes */
+ protected long[] plcValB;
+
+
+ private QCPLCBit(String thingType, String bitType, byte[] data) {
+ super(thingType, bitType, data);
+
+ // First four bytes are the number
+ numberOfPLCs = (int)LittleEndian.getUInt(data, 0);
+
+ // Next four bytes are the type
+ typeOfPLCS = (int)LittleEndian.getUInt(data, 4);
+
+ // Init the arrays that we can
+ plcValA = new long[numberOfPLCs];
+ plcValB = new long[numberOfPLCs];
+ }
+
+
+
+ public int getNumberOfPLCs() {
+ return numberOfPLCs;
+ }
+ public int getTypeOfPLCS() {
+ return typeOfPLCS;
+ }
+
+ public int[] getPreData() {
+ return preData;
+ }
+
+ public long[] getPlcValA() {
+ return plcValA;
+ }
+ public long[] getPlcValB() {
+ return plcValB;
+ }
+
+
+
+ public static QCPLCBit createQCPLCBit(String thingType, String bitType, byte[] data) {
+ // Grab the type
+ int type = (int)LittleEndian.getUInt(data, 4);
+ switch(type) {
+ case 0:
+ return new Type0(thingType, bitType, data);
+ case 4:
+ return new Type4(thingType, bitType, data);
+ case 8:
+ return new Type8(thingType, bitType, data);
+ case 12: // 0xc
+ return new Type12(thingType, bitType, data);
+ default:
+ throw new IllegalArgumentException("Sorry, I don't know how to deal with PLCs of type " + type);
+ }
+ }
+
+
+ /**
+ * Type 0 seem to be somewhat rare. They have 8 bytes of pre-data,
+ * then 2x 2 byte values.
+ */
+ public static class Type0 extends QCPLCBit {
+ private Type0(String thingType, String bitType, byte[] data) {
+ super(thingType, bitType, data);
+
+ // Grab our 4x pre-data
+ preData = new int[4];
+ preData[0] = LittleEndian.getUShort(data, 8+0);
+ preData[1] = LittleEndian.getUShort(data, 8+2);
+ preData[2] = LittleEndian.getUShort(data, 8+4);
+ preData[3] = LittleEndian.getUShort(data, 8+6);
+
+ // And grab the 2 byte values
+ for(int i=0; i<numberOfPLCs; i++) {
+ plcValA[i] = LittleEndian.getUShort(data, 16+(4*i));
+ plcValB[i] = LittleEndian.getUShort(data, 16+(4*i)+2);
+ }
+ }
+ }
+
+ /**
+ * Type 4 is quite common. They have 8 bytes of pre-data,
+ * then 2x 4 byte values.
+ */
+ public static class Type4 extends QCPLCBit {
+ private Type4(String thingType, String bitType, byte[] data) {
+ super(thingType, bitType, data);
+
+ // Grab our 4x pre-data
+ preData = new int[4];
+ preData[0] = LittleEndian.getUShort(data, 8+0);
+ preData[1] = LittleEndian.getUShort(data, 8+2);
+ preData[2] = LittleEndian.getUShort(data, 8+4);
+ preData[3] = LittleEndian.getUShort(data, 8+6);
+
+ // And grab the 4 byte values
+ for(int i=0; i<numberOfPLCs; i++) {
+ plcValA[i] = LittleEndian.getUInt(data, 16+(8*i));
+ plcValB[i] = LittleEndian.getUInt(data, 16+(8*i)+4);
+ }
+ }
+ }
+
+ /**
+ * Type 8 is quite common. They have 14 bytes of pre-data,
+ * then 2x 4 byte values.
+ */
+ public static class Type8 extends QCPLCBit {
+ private Type8(String thingType, String bitType, byte[] data) {
+ super(thingType, bitType, data);
+
+ // Grab our 7x pre-data
+ preData = new int[7];
+ preData[0] = LittleEndian.getUShort(data, 8+0);
+ preData[1] = LittleEndian.getUShort(data, 8+2);
+ preData[2] = LittleEndian.getUShort(data, 8+4);
+ preData[3] = LittleEndian.getUShort(data, 8+6);
+ preData[4] = LittleEndian.getUShort(data, 8+8);
+ preData[5] = LittleEndian.getUShort(data, 8+10);
+ preData[6] = LittleEndian.getUShort(data, 8+12);
+
+ // And grab the 4 byte values
+ for(int i=0; i<numberOfPLCs; i++) {
+ plcValA[i] = LittleEndian.getUInt(data, 22+(8*i));
+ plcValB[i] = LittleEndian.getUInt(data, 22+(8*i)+4);
+ }
+ }
+ }
+
+ /**
+ * Type 12 holds hyperlinks, and is very complex.
+ * There is normally one of these for each text
+ * area that contains at least one hyperlinks.
+ * The character offsets are relative to the start
+ * of the text area that this applies to.
+ */
+ public static class Type12 extends QCPLCBit {
+ private String[] hyperlinks;
+
+ private static final int oneStartsAt = 0x4c;
+ private static final int twoStartsAt = 0x68;
+ private static final int threePlusIncrement = 22;
+
+ private Type12(String thingType, String bitType, byte[] data) {
+ super(thingType, bitType, data);
+
+ // How many hyperlinks do we really have?
+ // (zero hyperlinks gets numberOfPLCs=1)
+ if(data.length == 0x34) {
+ hyperlinks = new String[0];
+ } else {
+ hyperlinks = new String[numberOfPLCs];
+ }
+
+ // We have 4 bytes, then the start point of each
+ // hyperlink, then the end point of the text.
+ preData = new int[1+numberOfPLCs+1];
+ for(int i=0; i<preData.length; i++) {
+ preData[i] = (int)LittleEndian.getUInt(data, 8+(i*4));
+ }
+
+ // Then we have a whole bunch of stuff, which grows
+ // with the number of hyperlinks
+ // For now, we think these are shorts
+ int at = 8+4+(numberOfPLCs*4)+4;
+ int until = 0x34;
+ if(numberOfPLCs == 1 && hyperlinks.length == 1) {
+ until = oneStartsAt;
+ } else if(numberOfPLCs >= 2) {
+ until = twoStartsAt + (numberOfPLCs-2)*threePlusIncrement;
+ }
+
+ plcValA = new long[(until-at)/2];
+ plcValB = new long[0];
+ for(int i=0; i<plcValA.length; i++) {
+ plcValA[i] = LittleEndian.getUShort(data, at+(i*2));
+ }
+
+ // Finally, we have a series of lengths + hyperlinks
+ at = until;
+ for(int i=0; i<hyperlinks.length; i++) {
+ int len = LittleEndian.getUShort(data, at);
+ int first = LittleEndian.getUShort(data, at+2);
+ if(first == 0) {
+ // Crazy special case
+ // Length is in bytes, from the start
+ // Hyperlink appears to be empty
+ hyperlinks[i] = "";
+ at += len;
+ } else {
+ // Normal case. Length is in characters
+ hyperlinks[i] = StringUtil.getFromUnicodeLE(data, at+2, len);
+ at += 2 + (2*len);
+ }
+ }
+ }
+
+ /**
+ * Returns the number of hyperlinks, which should
+ * either be zero, or the number of PLC bits
+ */
+ public int getNumberOfHyperlinks() {
+ return hyperlinks.length;
+ }
+
+ /**
+ * Returns the URL of the hyperlink at the
+ * given index.
+ * @param number The hyperlink number, zero based
+ */
+ public String getHyperlink(int number) {
+ return hyperlinks[number];
+ }
+ /**
+ * Returns where in the text (in characters) the
+ * hyperlink at the given index starts
+ * applying to.
+ * This position is relative to the text area that this
+ * PLCBit applies to.
+ * @param number The hyperlink number, zero based
+ */
+ public int getTextStartAt(int number) {
+ return preData[1+number];
+ }
+ /**
+ * Returns where in the text that this block
+ * of hyperlinks stops applying to. Normally,
+ * but not always the end of the text.
+ * This position is relative to the text area that this
+ * PLCBit applies to.
+ */
+ public int getAllTextEndAt() {
+ return preData[numberOfPLCs+1];
+ }
+ }
+}
assertEquals(s2007, s2000);
assertEquals(s2007, s98);
}
+
+ /**
+ * Test that the hyperlink extraction stuff works as well
+ * as we can hope it to.
+ */
+ public void testWithHyperlinks() throws Exception {
+ File f = new File(dir, "LinkAt10.pub");
+ HPBFDocument doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+
+ PublisherTextExtractor ext =
+ new PublisherTextExtractor(doc);
+ ext.getText();
+
+ // Default is no hyperlinks
+ assertEquals("1234567890LINK\n", ext.getText());
+
+ // Turn on
+ ext.setHyperlinksByDefault(true);
+ assertEquals("1234567890LINK\n<http://poi.apache.org/>\n", ext.getText());
+
+
+ // Now a much more complex document
+ f = new File(dir, "Sample.pub");
+ ext = new PublisherTextExtractor(new FileInputStream(f));
+ ext.setHyperlinksByDefault(true);
+ String text = ext.getText();
+
+ assertTrue(text.endsWith(
+ "<http://poi.apache.org/>\n" +
+ "<C:\\Documents and Settings\\Nick\\My Documents\\Booleans.xlsx>\n" +
+ "<>\n" +
+ "<mailto:dev@poi.apache.org?subject=HPBF>\n" +
+ "<mailto:dev@poi.apache.org?subject=HPBF>\n"
+ ));
+ }
}
import org.apache.poi.hpbf.HPBFDocument;
import org.apache.poi.hpbf.model.qcbits.QCTextBit;
+import org.apache.poi.hpbf.model.qcbits.QCPLCBit.Type12;
+import org.apache.poi.hpbf.model.qcbits.QCPLCBit.Type0;
+import org.apache.poi.hpbf.model.qcbits.QCPLCBit.Type4;
+import org.apache.poi.hpbf.model.qcbits.QCPLCBit.Type8;
import junit.framework.TestCase;
assertTrue(t.startsWith("This is some text on the first page"));
assertTrue(t.endsWith("Within doc to page 1\r"));
}
+
+ public void testPLC() throws Exception {
+ File f = new File(dir, "Simple.pub");
+ HPBFDocument doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+
+ QuillContents qc = doc.getQuillContents();
+ assertEquals(20, qc.getBits().length);
+
+ assertTrue(qc.getBits()[9] instanceof Type4);
+ assertTrue(qc.getBits()[10] instanceof Type4);
+ assertTrue(qc.getBits()[12] instanceof Type8);
+
+ Type4 plc9 = (Type4)qc.getBits()[9];
+ Type4 plc10 = (Type4)qc.getBits()[10];
+ Type8 plc12 = (Type8)qc.getBits()[12];
+
+
+ assertEquals(1, plc9.getNumberOfPLCs());
+ assertEquals(4, plc9.getPreData().length);
+ assertEquals(1, plc9.getPlcValA().length);
+ assertEquals(1, plc9.getPlcValB().length);
+
+ assertEquals(0, plc9.getPreData()[0]);
+ assertEquals(0, plc9.getPreData()[1]);
+ assertEquals(0, plc9.getPreData()[2]);
+ assertEquals(0, plc9.getPreData()[3]);
+ assertEquals(0x356, plc9.getPlcValA()[0]);
+ assertEquals(0x600, plc9.getPlcValB()[0]);
+
+
+ assertEquals(1, plc10.getNumberOfPLCs());
+ assertEquals(4, plc10.getPreData().length);
+ assertEquals(1, plc10.getPlcValA().length);
+ assertEquals(1, plc10.getPlcValB().length);
+
+ assertEquals(0, plc10.getPreData()[0]);
+ assertEquals(0, plc10.getPreData()[1]);
+ assertEquals(0, plc10.getPreData()[2]);
+ assertEquals(0, plc10.getPreData()[3]);
+ assertEquals(0x356, plc10.getPlcValA()[0]);
+ assertEquals(0x800, plc10.getPlcValB()[0]);
+
+ assertEquals(2, plc12.getNumberOfPLCs());
+ assertEquals(7, plc12.getPreData().length);
+ assertEquals(2, plc12.getPlcValA().length);
+ assertEquals(2, plc12.getPlcValB().length);
+
+ assertEquals(0xff, plc12.getPreData()[0]);
+ assertEquals(0, plc12.getPreData()[1]);
+ assertEquals(0x3d, plc12.getPreData()[2]);
+ assertEquals(0, plc12.getPreData()[3]);
+ assertEquals(0x6e, plc12.getPreData()[4]);
+ assertEquals(0, plc12.getPreData()[5]);
+ assertEquals(0, plc12.getPreData()[6]);
+ assertEquals(0xa0000, plc12.getPlcValA()[0]);
+ assertEquals(0x22000000, plc12.getPlcValB()[0]);
+ assertEquals(0x05, plc12.getPlcValA()[1]);
+ assertEquals(0x04, plc12.getPlcValB()[1]);
+ }
+
+ public void testComplexPLC() throws Exception {
+ File f = new File(dir, "Sample.pub");
+ HPBFDocument doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+
+ QuillContents qc = doc.getQuillContents();
+ assertEquals(20, qc.getBits().length);
+
+ assertTrue(qc.getBits()[10] instanceof Type4);
+ assertTrue(qc.getBits()[11] instanceof Type4);
+ assertTrue(qc.getBits()[13] instanceof Type0);
+ assertTrue(qc.getBits()[14] instanceof Type12);
+ assertTrue(qc.getBits()[15] instanceof Type12);
+ assertTrue(qc.getBits()[16] instanceof Type8);
+
+ Type4 plc10 = (Type4)qc.getBits()[10];
+ Type4 plc11 = (Type4)qc.getBits()[11];
+ Type0 plc13 = (Type0)qc.getBits()[13];
+ Type12 plc14 = (Type12)qc.getBits()[14];
+ Type12 plc15 = (Type12)qc.getBits()[15];
+ Type8 plc16 = (Type8)qc.getBits()[16];
+
+
+ assertEquals(1, plc10.getNumberOfPLCs());
+ assertEquals(4, plc10.getPreData().length);
+ assertEquals(1, plc10.getPlcValA().length);
+ assertEquals(1, plc10.getPlcValB().length);
+
+ assertEquals(0, plc10.getPreData()[0]);
+ assertEquals(0, plc10.getPreData()[1]);
+ assertEquals(0, plc10.getPreData()[2]);
+ assertEquals(0, plc10.getPreData()[3]);
+ assertEquals(0x5d0, plc10.getPlcValA()[0]);
+ assertEquals(0x800, plc10.getPlcValB()[0]);
+
+
+ assertEquals(2, plc11.getNumberOfPLCs());
+ assertEquals(4, plc11.getPreData().length);
+ assertEquals(2, plc11.getPlcValA().length);
+ assertEquals(2, plc11.getPlcValB().length);
+
+ assertEquals(0, plc11.getPreData()[0]);
+ assertEquals(0, plc11.getPreData()[1]);
+ assertEquals(0, plc11.getPreData()[2]);
+ assertEquals(0, plc11.getPreData()[3]);
+ assertEquals(0x53a, plc11.getPlcValA()[0]);
+ assertEquals(0x5d0, plc11.getPlcValB()[0]);
+ assertEquals(0xa00, plc11.getPlcValA()[1]);
+ assertEquals(0xc00, plc11.getPlcValB()[1]);
+
+
+ assertEquals(5, plc13.getNumberOfPLCs());
+ assertEquals(4, plc13.getPreData().length);
+ assertEquals(5, plc13.getPlcValA().length);
+ assertEquals(5, plc13.getPlcValB().length);
+
+ assertEquals(0xff00, plc13.getPreData()[0]);
+ assertEquals(0, plc13.getPreData()[1]);
+ assertEquals(0xf, plc13.getPreData()[2]);
+ assertEquals(0, plc13.getPreData()[3]);
+ assertEquals(0x19, plc13.getPlcValA()[0]);
+ assertEquals(0x00, plc13.getPlcValB()[0]);
+ assertEquals(0x27, plc13.getPlcValA()[1]);
+ assertEquals(0x00, plc13.getPlcValB()[1]);
+ assertEquals(0x36, plc13.getPlcValA()[2]);
+ assertEquals(0x00, plc13.getPlcValB()[2]);
+ assertEquals(0x42, plc13.getPlcValA()[3]);
+ assertEquals(0x00, plc13.getPlcValB()[3]);
+ assertEquals(0x50, plc13.getPlcValA()[4]);
+ assertEquals(0x00, plc13.getPlcValB()[4]);
+
+
+ // TODO - test the type 12s
+
+
+ assertEquals(6, plc16.getNumberOfPLCs());
+ assertEquals(7, plc16.getPreData().length);
+ assertEquals(6, plc16.getPlcValA().length);
+ assertEquals(6, plc16.getPlcValB().length);
+
+ assertEquals(0xff, plc16.getPreData()[0]);
+ assertEquals(0, plc16.getPreData()[1]);
+ assertEquals(0x56, plc16.getPreData()[2]);
+ assertEquals(0, plc16.getPreData()[3]);
+ assertEquals(0x62, plc16.getPreData()[4]);
+ assertEquals(0, plc16.getPreData()[5]);
+ assertEquals(0x3e, plc16.getPreData()[6]);
+ assertEquals(0x500000, plc16.getPlcValA()[0]);
+ assertEquals(0x570000, plc16.getPlcValB()[0]);
+ assertEquals(0x4b0000, plc16.getPlcValA()[1]);
+ assertEquals(0x000000, plc16.getPlcValB()[1]);
+ assertEquals(0x0a0000, plc16.getPlcValA()[2]);
+ assertEquals(0x22000000, plc16.getPlcValB()[2]);
+ assertEquals(0x000005, plc16.getPlcValA()[3]);
+ assertEquals(0x000004, plc16.getPlcValB()[3]);
+ assertEquals(0x000004, plc16.getPlcValA()[4]);
+ assertEquals(0x000004, plc16.getPlcValB()[4]);
+ assertEquals(0x000004, plc16.getPlcValA()[5]);
+ assertEquals(0x000004, plc16.getPlcValB()[5]);
+ }
+
+ public void testNoHyperlinks() throws Exception {
+ File f = new File(dir, "SampleNewsletter.pub");
+ HPBFDocument doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+
+ QuillContents qc = doc.getQuillContents();
+ assertEquals(20, qc.getBits().length);
+
+ Type12 plc18 = (Type12)qc.getBits()[18];
+
+ assertEquals(1, plc18.getNumberOfPLCs());
+ assertEquals(0, plc18.getNumberOfHyperlinks());
+ assertEquals(0, plc18.getTextStartAt(0));
+ assertEquals(601, plc18.getAllTextEndAt());
+ }
+
+ public void testSimpleHyperlink() throws Exception {
+ File f;
+ HPBFDocument doc;
+ QuillContents qc;
+ Type12 hlBit;
+
+ // Link at 10
+ f = new File(dir, "LinkAt10.pub");
+ doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+ qc = doc.getQuillContents();
+
+ hlBit = (Type12)qc.getBits()[12];
+ assertEquals(1, hlBit.getNumberOfPLCs());
+ assertEquals(1, hlBit.getNumberOfHyperlinks());
+
+ assertEquals(10, hlBit.getTextStartAt(0));
+ assertEquals(15, hlBit.getAllTextEndAt());
+ assertEquals("http://poi.apache.org/", hlBit.getHyperlink(0));
+
+ // Longer link at 10
+ f = new File(dir, "LinkAt10Longer.pub");
+ doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+ qc = doc.getQuillContents();
+
+ hlBit = (Type12)qc.getBits()[12];
+ assertEquals(1, hlBit.getNumberOfPLCs());
+ assertEquals(1, hlBit.getNumberOfHyperlinks());
+
+ assertEquals(10, hlBit.getTextStartAt(0));
+ assertEquals(15, hlBit.getAllTextEndAt());
+ assertEquals("http://poi.apache.org/hpbf/", hlBit.getHyperlink(0));
+
+ // Link at 20
+ f = new File(dir, "LinkAt20.pub");
+ doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+ qc = doc.getQuillContents();
+
+ hlBit = (Type12)qc.getBits()[12];
+ assertEquals(1, hlBit.getNumberOfPLCs());
+ assertEquals(1, hlBit.getNumberOfHyperlinks());
+
+ assertEquals(20, hlBit.getTextStartAt(0));
+ assertEquals(25, hlBit.getAllTextEndAt());
+ assertEquals("http://poi.apache.org/", hlBit.getHyperlink(0));
+ }
+
+ public void testManyHyperlinks() throws Exception {
+ File f;
+ HPBFDocument doc;
+ QuillContents qc;
+ Type12 hlBit;
+
+ // Link at 10
+ f = new File(dir, "LinkAt10.pub");
+ doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+ qc = doc.getQuillContents();
+
+ hlBit = (Type12)qc.getBits()[12];
+ assertEquals(1, hlBit.getNumberOfPLCs());
+ assertEquals(1, hlBit.getNumberOfHyperlinks());
+
+ assertEquals(10, hlBit.getTextStartAt(0));
+ assertEquals(15, hlBit.getAllTextEndAt());
+ assertEquals("http://poi.apache.org/", hlBit.getHyperlink(0));
+
+ }
+
+ public void testHyperlinkDifferentVersions() throws Exception {
+ File f;
+ HPBFDocument doc;
+ QuillContents qc;
+ Type12 hlBitA;
+ Type12 hlBitB;
+
+ // Latest version
+ f = new File(dir, "Sample.pub");
+ doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+ qc = doc.getQuillContents();
+
+ hlBitA = (Type12)qc.getBits()[14];
+ assertEquals(2, hlBitA.getNumberOfPLCs());
+ assertEquals(2, hlBitA.getNumberOfHyperlinks());
+
+ assertEquals(25, hlBitA.getTextStartAt(0));
+ assertEquals(72, hlBitA.getTextStartAt(1));
+ assertEquals(87, hlBitA.getAllTextEndAt());
+ assertEquals("http://poi.apache.org/", hlBitA.getHyperlink(0));
+ assertEquals("C:\\Documents and Settings\\Nick\\My Documents\\Booleans.xlsx", hlBitA.getHyperlink(1));
+
+ hlBitB = (Type12)qc.getBits()[15];
+ assertEquals(3, hlBitB.getNumberOfPLCs());
+ assertEquals(3, hlBitB.getNumberOfHyperlinks());
+
+ assertEquals(27, hlBitB.getTextStartAt(0));
+ assertEquals(37, hlBitB.getTextStartAt(1));
+ assertEquals(54, hlBitB.getTextStartAt(2));
+ assertEquals(75, hlBitB.getAllTextEndAt());
+ assertEquals("", hlBitB.getHyperlink(0));
+ assertEquals("mailto:dev@poi.apache.org?subject=HPBF", hlBitB.getHyperlink(1));
+ assertEquals("mailto:dev@poi.apache.org?subject=HPBF", hlBitB.getHyperlink(2));
+
+ // 2000 version
+ f = new File(dir, "Sample2000.pub");
+ doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+ qc = doc.getQuillContents();
+
+ hlBitA = (Type12)qc.getBits()[13];
+ assertEquals(2, hlBitA.getNumberOfPLCs());
+ assertEquals(2, hlBitA.getNumberOfHyperlinks());
+
+ assertEquals(25, hlBitA.getTextStartAt(0));
+ assertEquals(72, hlBitA.getTextStartAt(1));
+ assertEquals(87, hlBitA.getAllTextEndAt());
+ assertEquals("http://poi.apache.org/", hlBitA.getHyperlink(0));
+ assertEquals("C:\\Documents and Settings\\Nick\\My Documents\\Booleans.xlsx", hlBitA.getHyperlink(1));
+
+ hlBitB = (Type12)qc.getBits()[14];
+ assertEquals(3, hlBitB.getNumberOfPLCs());
+ assertEquals(3, hlBitB.getNumberOfHyperlinks());
+
+ assertEquals(27, hlBitB.getTextStartAt(0));
+ assertEquals(37, hlBitB.getTextStartAt(1));
+ assertEquals(54, hlBitB.getTextStartAt(2));
+ assertEquals(75, hlBitB.getAllTextEndAt());
+ assertEquals("", hlBitB.getHyperlink(0));
+ assertEquals("mailto:dev@poi.apache.org?subject=HPBF", hlBitB.getHyperlink(1));
+ assertEquals("mailto:dev@poi.apache.org?subject=HPBF", hlBitB.getHyperlink(2));
+
+ // 98 version
+ f = new File(dir, "Sample98.pub");
+ doc = new HPBFDocument(
+ new FileInputStream(f)
+ );
+ qc = doc.getQuillContents();
+
+ hlBitA = (Type12)qc.getBits()[13];
+ assertEquals(2, hlBitA.getNumberOfPLCs());
+ assertEquals(2, hlBitA.getNumberOfHyperlinks());
+
+ assertEquals(25, hlBitA.getTextStartAt(0));
+ assertEquals(72, hlBitA.getTextStartAt(1));
+ assertEquals(87, hlBitA.getAllTextEndAt());
+ assertEquals("http://poi.apache.org/", hlBitA.getHyperlink(0));
+ assertEquals("C:\\Documents and Settings\\Nick\\My Documents\\Booleans.xlsx", hlBitA.getHyperlink(1));
+
+ hlBitB = (Type12)qc.getBits()[14];
+ assertEquals(3, hlBitB.getNumberOfPLCs());
+ assertEquals(3, hlBitB.getNumberOfHyperlinks());
+
+ assertEquals(27, hlBitB.getTextStartAt(0));
+ assertEquals(37, hlBitB.getTextStartAt(1));
+ assertEquals(54, hlBitB.getTextStartAt(2));
+ assertEquals(75, hlBitB.getAllTextEndAt());
+ assertEquals("", hlBitB.getHyperlink(0));
+ assertEquals("mailto:dev@poi.apache.org?subject=HPBF", hlBitB.getHyperlink(1));
+ assertEquals("mailto:dev@poi.apache.org?subject=HPBF", hlBitB.getHyperlink(2));
+ }
}
package org.apache.poi.hssf.model;
-import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;
import junit.framework.TestCase;
import org.apache.poi.hssf.HSSFTestDataSamples;
-import org.apache.poi.hssf.eventmodel.ERFListener;
-import org.apache.poi.hssf.eventmodel.EventRecordFactory;
import org.apache.poi.hssf.record.BOFRecord;
import org.apache.poi.hssf.record.BlankRecord;
import org.apache.poi.hssf.record.CellValueRecordInterface;
import org.apache.poi.hssf.record.aggregates.MergedCellsTable;
import org.apache.poi.hssf.record.aggregates.PageSettingsBlock;
import org.apache.poi.hssf.record.aggregates.RowRecordsAggregate;
+import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
return result;
}
- private static final class MergedCellListener implements ERFListener {
+ private static final class MergedCellListener implements RecordVisitor {
private int _count;
public MergedCellListener() {
_count = 0;
}
- public boolean processRecord(Record rec) {
- _count++;
- return true;
+ public void visitRecord(Record r) {
+ if (r instanceof MergeCellsRecord) {
+ _count++;
+ }
}
public int getCount() {
return _count;
assertTrue(sheet.getNumMergedRegions() == regionsToAdd);
//test that the regions were spread out over the appropriate number of records
- byte[] sheetData = new byte[sheet.getSize()];
- sheet.serialize(0, sheetData);
MergedCellListener mcListener = new MergedCellListener();
- EventRecordFactory erf = new EventRecordFactory(mcListener, new short[] { MergeCellsRecord.sid, });
-// POIFSFileSystem poifs = new POIFSFileSystem(new ByteArrayInputStream(sheetData));
- erf.processRecords(new ByteArrayInputStream(sheetData));
+ sheet.visitContainedRecords(mcListener, 0);
int recordsAdded = mcListener.getCount();
int recordsExpected = regionsToAdd/1027;
if ((regionsToAdd % 1027) != 0)
assertEquals(DEFAULT_IDX, xfindex);
}
+ private static final class SizeCheckingRecordVisitor implements RecordVisitor {
+
+ private int _totalSize;
+ public SizeCheckingRecordVisitor() {
+ _totalSize = 0;
+ }
+ public void visitRecord(Record r) {
+
+ int estimatedSize=r.getRecordSize();
+ byte[] buf = new byte[estimatedSize];
+ int serializedSize = r.serialize(0, buf);
+ if (estimatedSize != serializedSize) {
+ throw new AssertionFailedError("serialized size mismatch for record ("
+ + r.getClass().getName() + ")");
+ }
+ _totalSize += estimatedSize;
+ }
+ public int getTotalSize() {
+ return _totalSize;
+ }
+ }
/**
* Prior to bug 45066, POI would get the estimated sheet size wrong
* when an <tt>UncalcedRecord</tt> was present.<p/>
records.add(createWindow2Record());
records.add(EOFRecord.instance);
Sheet sheet = Sheet.createSheet(records, 0, 0);
-
- int estimatedSize = sheet.getSize();
- int serializedSize = sheet.serialize(0, new byte[estimatedSize]);
- if (serializedSize != estimatedSize) {
- throw new AssertionFailedError("Identified bug 45066 b");
- }
- assertEquals(90, serializedSize);
+
+ // The original bug was due to different logic for collecting records for sizing and
+ // serialization. The code has since been refactored into a single method for visiting
+ // all contained records. Now this test is much less interesting
+ SizeCheckingRecordVisitor scrv = new SizeCheckingRecordVisitor();
+ sheet.visitContainedRecords(scrv, 0);
+ assertEquals(90, scrv.getTotalSize());
}
/**
* That value is found on the IndexRecord.
*/
private static int getDbCellRecordPos(Sheet sheet) {
- int size = sheet.getSize();
- byte[] data = new byte[size];
- sheet.serialize(0, data);
MyIndexRecordListener myIndexListener = new MyIndexRecordListener();
- EventRecordFactory erf = new EventRecordFactory(myIndexListener, new short[] { IndexRecord.sid, });
- erf.processRecords(new ByteArrayInputStream(data));
+ sheet.visitContainedRecords(myIndexListener, 0);
IndexRecord indexRecord = myIndexListener.getIndexRecord();
int dbCellRecordPos = indexRecord.getDbcellAt(0);
return dbCellRecordPos;
}
- private static final class MyIndexRecordListener implements ERFListener {
+ private static final class MyIndexRecordListener implements RecordVisitor {
private IndexRecord _indexRecord;
public MyIndexRecordListener() {
// no-arg constructor
}
- public boolean processRecord(Record rec) {
- _indexRecord = (IndexRecord)rec;
- return true;
- }
public IndexRecord getIndexRecord() {
return _indexRecord;
}
+ public void visitRecord(Record r) {
+ if (r instanceof IndexRecord) {
+ if (_indexRecord != null) {
+ throw new RuntimeException("too many index records");
+ }
+ _indexRecord = (IndexRecord)r;
+ }
+ }
}
/**
}
assertEquals("Informations", cell.getRichStringCellValue().getString());
}
+ /**
+ * In 3.1, setting margins between creating first row and first cell caused an exception.
+ */
+ public void testSetMargins_bug45717() {
+ HSSFWorkbook workbook = new HSSFWorkbook();
+ HSSFSheet sheet = workbook.createSheet("Vorschauliste");
+ HSSFRow row = sheet.createRow(0);
+
+ sheet.setMargin(HSSFSheet.LeftMargin, 0.3);
+ try {
+ row.createCell((short) 0);
+ } catch (IllegalStateException e) {
+ if (e.getMessage().equals("Cannot create value records before row records exist")) {
+ throw new AssertionFailedError("Identified bug 45717");
+ }
+ throw e;
+ }
+ }
}
limitations under the License.
==================================================================== */
-/*
- * TestFormulaRecordAggregate.java
- *
- * Created on March 21, 2003, 12:32 AM
- */
-
package org.apache.poi.hssf.record.aggregates;
+
+import junit.framework.TestCase;
+
import org.apache.poi.hssf.record.FormulaRecord;
import org.apache.poi.hssf.record.StringRecord;
*
* @author avik
*/
-public final class TestFormulaRecordAggregate extends junit.framework.TestCase {
+public final class TestFormulaRecordAggregate extends TestCase {
public void testBasic() throws Exception {
FormulaRecord f = new FormulaRecord();
StringRecord s = new StringRecord();
s.setString("abc");
- FormulaRecordAggregate fagg = new FormulaRecordAggregate(f);
- fagg.setStringRecord(s);
+ FormulaRecordAggregate fagg = new FormulaRecordAggregate(f, s, SharedValueManager.EMPTY);
assertEquals("abc", fagg.getStringValue());
}
}
package org.apache.poi.hssf.record.aggregates;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
+import org.apache.poi.hssf.HSSFTestDataSamples;
+import org.apache.poi.hssf.record.ArrayRecord;
+import org.apache.poi.hssf.record.FormulaRecord;
+import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.RowRecord;
+import org.apache.poi.hssf.record.SharedFormulaRecord;
+import org.apache.poi.hssf.record.SharedValueRecordBase;
+import org.apache.poi.hssf.record.TableRecord;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.hssf.usermodel.RecordInspector;
+import org.apache.poi.hssf.util.CellRangeAddress8Bit;
/**
*
*/
public final class TestRowRecordsAggregate extends TestCase {
-
- public void testRowGet() {
- RowRecordsAggregate rra = new RowRecordsAggregate();
- RowRecord rr = new RowRecord(4);
- rra.insertRow(rr);
- rra.insertRow(new RowRecord(1));
-
- RowRecord rr1 = rra.getRow(4);
-
- assertNotNull(rr1);
- assertEquals("Row number is 1", 4, rr1.getRowNumber());
- assertTrue("Row record retrieved is identical ", rr1 == rr);
- }
+
+ public void testRowGet() {
+ RowRecordsAggregate rra = new RowRecordsAggregate();
+ RowRecord rr = new RowRecord(4);
+ rra.insertRow(rr);
+ rra.insertRow(new RowRecord(1));
+
+ RowRecord rr1 = rra.getRow(4);
+
+ assertNotNull(rr1);
+ assertEquals("Row number is 1", 4, rr1.getRowNumber());
+ assertTrue("Row record retrieved is identical ", rr1 == rr);
+ }
+
+ /**
+ * Prior to Aug 2008, POI would re-serialize spreadsheets with {@link ArrayRecord}s or
+ * {@link TableRecord}s with those records out of order. Similar to
+ * {@link SharedFormulaRecord}s, these records should appear immediately after the first
+ * {@link FormulaRecord}s that they apply to (and only once).<br/>
+ */
+ public void testArraysAndTables() {
+ HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("testArraysAndTables.xls");
+ Record[] sheetRecs = RecordInspector.getRecords(wb.getSheetAt(0), 0);
+
+ int countArrayFormulas = verifySharedValues(sheetRecs, ArrayRecord.class);
+ assertEquals(5, countArrayFormulas);
+ int countTableFormulas = verifySharedValues(sheetRecs, TableRecord.class);
+ assertEquals(3, countTableFormulas);
+
+ // Note - SharedFormulaRecords are currently not re-serialized by POI (each is extracted
+ // into many non-shared formulas), but if they ever were, the same rules would apply.
+ int countSharedFormulas = verifySharedValues(sheetRecs, SharedFormulaRecord.class);
+ assertEquals(0, countSharedFormulas);
+
+
+ if (false) { // set true to observe re-serialized file
+ File f = new File(System.getProperty("java.io.tmpdir") + "/testArraysAndTables-out.xls");
+ try {
+ OutputStream os = new FileOutputStream(f);
+ wb.write(os);
+ os.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ System.out.println("Output file to " + f.getAbsolutePath());
+ }
+ }
+
+ private static int verifySharedValues(Record[] recs, Class shfClass) {
+
+ int result =0;
+ for(int i=0; i<recs.length; i++) {
+ Record rec = recs[i];
+ if (rec.getClass() == shfClass) {
+ result++;
+ Record prevRec = recs[i-1];
+ if (!(prevRec instanceof FormulaRecord)) {
+ throw new AssertionFailedError("Bad record order at index "
+ + i + ": Formula record expected but got ("
+ + prevRec.getClass().getName() + ")");
+ }
+ verifySharedFormula((FormulaRecord) prevRec, rec);
+ }
+ }
+ return result;
+ }
+
+ private static void verifySharedFormula(FormulaRecord firstFormula, Record rec) {
+ CellRangeAddress8Bit range = ((SharedValueRecordBase)rec).getRange();
+ assertEquals(range.getFirstRow(), firstFormula.getRow());
+ assertEquals(range.getFirstColumn(), firstFormula.getColumn());
+ }
}
import junit.framework.TestCase;
import org.apache.poi.hssf.HSSFTestDataSamples;
+import org.apache.poi.hssf.model.RecordStream;
+import org.apache.poi.hssf.model.RowBlocksReader;
import org.apache.poi.hssf.record.BlankRecord;
+import org.apache.poi.hssf.record.CellValueRecordInterface;
import org.apache.poi.hssf.record.FormulaRecord;
+import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.RecordBase;
import org.apache.poi.hssf.record.SharedFormulaRecord;
+import org.apache.poi.hssf.record.WindowTwoRecord;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
List records = new ArrayList();
records.add( new FormulaRecord() );
records.add( new SharedFormulaRecord() );
+ records.add(new WindowTwoRecord());
constructValueRecord(records);
Iterator iterator = valueRecord.getIterator();
}
private void constructValueRecord(List records) {
- SharedFormulaHolder sfrh = SharedFormulaHolder.create(records, 0, records.size());
- valueRecord.construct(records, 0, records.size(), sfrh );
+ RowBlocksReader rbr = new RowBlocksReader(records, 0);
+ SharedValueManager sfrh = rbr.getSharedFormulaManager();
+ RecordStream rs = rbr.getPlainRecordStream();
+ while(rs.hasNext()) {
+ Record rec = rs.getNext();
+ valueRecord.construct((CellValueRecordInterface)rec, rs, sfrh);
+ }
}
private static List testData() {
blankRecord.setColumn( (short) 2 );
records.add( formulaRecord );
records.add( blankRecord );
+ records.add(new WindowTwoRecord());
return records;
}
--- /dev/null
+/* ====================================================================
+ 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.hssf.usermodel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.poi.hssf.record.Record;
+import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
+
+/**
+ * Test utility class to get {@link Record}s out HSSF objects
+ *
+ * @author Josh Micich
+ */
+public final class RecordInspector {
+
+ private RecordInspector() {
+ // no instances of this class
+ }
+
+ private static final class RecordCollector implements RecordVisitor {
+
+ private List _list;
+
+ public RecordCollector() {
+ _list = new ArrayList(128);
+ }
+
+ public void visitRecord(Record r) {
+ _list.add(r);
+ }
+
+ public Record[] getRecords() {
+ Record[] result = new Record[_list.size()];
+ _list.toArray(result);
+ return result;
+ }
+ }
+
+ /**
+ * @param streamOffset start position for serialization. This affects values in some
+ * records such as INDEX, but most callers will be OK to pass zero.
+ * @return the {@link Record}s (in order) which will be output when the
+ * specified sheet is serialized
+ */
+ public static Record[] getRecords(HSSFSheet hSheet, int streamOffset) {
+ RecordCollector rc = new RecordCollector();
+ hSheet.getSheet().visitContainedRecords(rc, streamOffset);
+ return rc.getRecords();
+ }
+}
package org.apache.poi.hssf.usermodel;
-import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.util.Iterator;
import junit.framework.TestCase;
import org.apache.poi.hssf.HSSFTestDataSamples;
-import org.apache.poi.hssf.eventmodel.ERFListener;
-import org.apache.poi.hssf.eventmodel.EventRecordFactory;
import org.apache.poi.hssf.model.Workbook;
import org.apache.poi.hssf.record.BackupRecord;
import org.apache.poi.hssf.record.LabelSSTRecord;
import org.apache.poi.hssf.record.Record;
-import org.apache.poi.hssf.record.aggregates.RowRecordsAggregate;
+import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.Region;
* HSSFSheet last row or first row is incorrect. <P>
*
*/
-
- public void testWriteSheetSimple()
- throws IOException
- {
+ public void testWriteSheetSimple() throws IOException {
File file = TempFile.createTempFile("testWriteSheetSimple",
".xls");
FileOutputStream out = new FileOutputStream(file);
HSSFRow r = null;
HSSFCell c = null;
- for (short rownum = ( short ) 0; rownum < 100; rownum++)
- {
+ for (int rownum = 0; rownum < 100; rownum++) {
r = s.createRow(rownum);
- // r.setRowNum(( short ) rownum);
- for (short cellnum = ( short ) 0; cellnum < 50; cellnum += 2)
- {
+ for (int cellnum = 0; cellnum < 50; cellnum += 2) {
c = r.createCell(cellnum);
c.setCellValue(rownum * 10000 + cellnum
+ ((( double ) rownum / 1000)
sanityChecker.checkHSSFWorkbook(wb);
assertEquals("LAST ROW == 99", 99, s.getLastRowNum());
assertEquals("FIRST ROW == 0", 0, s.getFirstRowNum());
-
- // assert((s.getLastRowNum() == 99));
}
/**
HSSFRow r = null;
HSSFCell c = null;
- for (short rownum = ( short ) 0; rownum < 100; rownum++)
- {
+ for (int rownum = 0; rownum < 100; rownum++) {
r = s.createRow(rownum);
- // r.setRowNum(( short ) rownum);
- for (short cellnum = ( short ) 0; cellnum < 50; cellnum += 2)
- {
+ for (int cellnum = 0; cellnum < 50; cellnum += 2) {
c = r.createCell(cellnum);
c.setCellValue(rownum * 10000 + cellnum
+ ((( double ) rownum / 1000)
c.setCellValue(new HSSFRichTextString("TEST"));
}
}
- for (short rownum = ( short ) 0; rownum < 25; rownum++)
- {
+ for (int rownum = 0; rownum < 25; rownum++) {
r = s.getRow(rownum);
s.removeRow(r);
}
- for (short rownum = ( short ) 75; rownum < 100; rownum++)
- {
+ for (int rownum = 75; rownum < 100; rownum++) {
r = s.getRow(rownum);
s.removeRow(r);
}
HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet s = wb.createSheet();
- for (short rownum = ( short ) 0; rownum < 100; rownum++)
- {
+ for (int rownum = 0; rownum < 100; rownum++) {
HSSFRow r = s.createRow(rownum);
- for (short cellnum = ( short ) 0; cellnum < 50; cellnum += 2)
- {
+ for (int cellnum = 0; cellnum < 50; cellnum += 2) {
HSSFCell c = r.createCell(cellnum);
c.setCellValue(rownum * 10000 + cellnum
+ ((( double ) rownum / 1000)
/**
* Test the backup field gets set as expected.
*/
-
- public void testBackupRecord()
- throws Exception
- {
- HSSFWorkbook wb = new HSSFWorkbook();
- wb.createSheet();
- Workbook workbook = wb.getWorkbook();
+ public void testBackupRecord() {
+ HSSFWorkbook wb = new HSSFWorkbook();
+ wb.createSheet();
+ Workbook workbook = wb.getWorkbook();
BackupRecord record = workbook.getBackupRecord();
assertEquals(0, record.getBackup());
assertEquals(1, record.getBackup());
}
- private static final class RecordCounter implements ERFListener {
+ private static final class RecordCounter implements RecordVisitor {
private int _count;
public RecordCounter() {
public int getCount() {
return _count;
}
- public boolean processRecord(Record rec) {
- _count++;
- return true;
+ public void visitRecord(Record r) {
+ if (r instanceof LabelSSTRecord) {
+ _count++;
+ }
}
}
*
* We need to make sure only one LabelSSTRecord is produced.
*/
- public void testRepeatingBug()
- throws Exception
- {
+ public void testRepeatingBug() {
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet("Design Variants");
HSSFRow row = sheet.createRow(2);
cell.setCellValue(new HSSFRichTextString("Class"));
cell = row.createCell(2);
- byte[] data = new byte[sheet.getSheet().getSize()];
- sheet.getSheet().serialize(0, data);
RecordCounter rc = new RecordCounter();
- EventRecordFactory erf = new EventRecordFactory(rc, new short[] { LabelSSTRecord.sid, });
- erf.processRecords(new ByteArrayInputStream(data));
-
+ sheet.getSheet().visitContainedRecords(rc, 0);
assertEquals(1, rc.getCount());
}