diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 36f01ceea5..c0f06eb8bd 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -67,6 +67,9 @@ Created a common interface for handling Excel files, irrespective of if they are .xls or .xlsx + 45912 - fixed ArrayIndexOutOfBoundsException in EmbeddedObjectRefSubRecord + 45889 - fixed ArrayIndexOutOfBoundsException when constructing HSLF Table with a single row + Initial support for creating hyperlinks in HSLF 45876 - fixed BoundSheetRecord to allow sheet names longer than 31 chars 45890 - fixed HSSFSheet.shiftRows to also update conditional formats 45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index bcf430664c..9a4b02f57f 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -64,6 +64,9 @@ Created a common interface for handling Excel files, irrespective of if they are .xls or .xlsx + 45912 - fixed ArrayIndexOutOfBoundsException in EmbeddedObjectRefSubRecord + 45889 - fixed ArrayIndexOutOfBoundsException when constructing HSLF Table with a single row + Initial support for creating hyperlinks in HSLF 45876 - fixed BoundSheetRecord to allow sheet names longer than 31 chars 45890 - fixed HSSFSheet.shiftRows to also update conditional formats 45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas diff --git a/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java b/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java index fb6bce5d3a..9e8d998b11 100644 --- a/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java +++ b/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -16,166 +15,312 @@ limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.record; +import java.io.ByteArrayInputStream; - -import org.apache.poi.util.*; +import org.apache.poi.hssf.record.formula.Area3DPtg; +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.Ref3DPtg; +import org.apache.poi.hssf.record.formula.RefPtg; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.StringUtil; /** + * ftPictFmla (0x0009)
* A sub-record within the OBJ record which stores a reference to an object * stored in a separate entry within the OLE2 compound file. * * @author Daniel Noll */ -public class EmbeddedObjectRefSubRecord - extends SubRecord -{ - public static final short sid = 0x9; +public final class EmbeddedObjectRefSubRecord extends SubRecord { + public static final short sid = 0x0009; - public short field_1_stream_id_offset; // Offset to stream ID from the point after this value. - public short[] field_2_unknown; // Unknown stuff at the front. TODO: Confirm that it's a short[] - // TODO: Consider making a utility class for these. I've discovered the same field ordering - // in FormatRecord and StringRecord, it may be elsewhere too. - public short field_3_unicode_len; // Length of Unicode string. - public boolean field_4_unicode_flag; // Flags whether the string is Unicode. - public String field_5_ole_classname; // Classname of the embedded OLE document (e.g. Word.Document.8) - public int field_6_stream_id; // ID of the OLE stream containing the actual data. + private static final byte[] EMPTY_BYTE_ARRAY = { }; - private int field_5_ole_classname_padding; // developer laziness... - public byte[] remainingBytes; + private int field_1_unknown_int; + /** either an area or a cell ref */ + private Ptg field_2_refPtg; + private byte[] field_2_unknownFormulaData; + // TODO: Consider making a utility class for these. I've discovered the same field ordering + // in FormatRecord and StringRecord, it may be elsewhere too. + private boolean field_3_unicode_flag; // Flags whether the string is Unicode. + private String field_4_ole_classname; // Classname of the embedded OLE document (e.g. Word.Document.8) + /** Formulas often have a single non-zero trailing byte. + * This is in a similar position to he pre-streamId padding + * It is unknown if the value is important (it seems to mirror a value a few bytes earlier) + * */ + private Byte field_4_unknownByte; + private Integer field_5_stream_id; // ID of the OLE stream containing the actual data. + private byte[] field_6_unknown; - public EmbeddedObjectRefSubRecord() - { - field_2_unknown = new short[0]; - remainingBytes = new byte[0]; - field_1_stream_id_offset = 6; - field_5_ole_classname = ""; - } - public short getSid() - { - return sid; - } + // currently for testing only - needs review + EmbeddedObjectRefSubRecord() { + field_2_unknownFormulaData = new byte[] { 0x02, 0x6C, 0x6A, 0x16, 0x01, }; // just some sample data. These values vary a lot + field_6_unknown = EMPTY_BYTE_ARRAY; + field_4_ole_classname = null; + } - public EmbeddedObjectRefSubRecord(RecordInputStream in) - { - field_1_stream_id_offset = in.readShort(); - field_2_unknown = in.readShortArray(); - field_3_unicode_len = in.readShort(); - field_4_unicode_flag = ( in.readByte() & 0x01 ) != 0; + public short getSid() { + return sid; + } - if ( field_4_unicode_flag ) - { - field_5_ole_classname = in.readUnicodeLEString( field_3_unicode_len ); - } - else - { - field_5_ole_classname = in.readCompressedUnicode( field_3_unicode_len ); - } + public EmbeddedObjectRefSubRecord(RecordInputStream in) { + // Much guess-work going on here due to lack of any documentation. + // See similar source code in OOO: + // http://lxr.go-oo.org/source/sc/sc/source/filter/excel/xiescher.cxx + // 1223 void XclImpOleObj::ReadPictFmla( XclImpStream& rStrm, sal_uInt16 nRecSize ) - // Padded with NUL bytes. The -2 is because field_1_stream_id_offset - // is relative to after the offset field, whereas in.getRecordOffset() - // is relative to the start of this record (minus the header.) - field_5_ole_classname_padding = 0; - while (in.getRecordOffset() - 2 < field_1_stream_id_offset) - { - field_5_ole_classname_padding++; - in.readByte(); // discard - } + int streamIdOffset = in.readShort(); // OOO calls this 'nFmlaLen' - // Fetch the stream ID - field_6_stream_id = in.readInt(); - - // Store what's left - remainingBytes = in.readRemainder(); - } + int dataLenAfterFormula = in.remaining() - streamIdOffset; + int formulaSize = in.readUShort(); + field_1_unknown_int = in.readInt(); + byte[] formulaRawBytes = readRawData(in, formulaSize); + field_2_refPtg = readRefPtg(formulaRawBytes); + if (field_2_refPtg == null) { + // common case + // field_2_n16 seems to be 5 here + // The formula almost looks like tTbl but the row/column values seem like garbage. + field_2_unknownFormulaData = formulaRawBytes; + } else { + field_2_unknownFormulaData = null; + } - public int serialize(int offset, byte[] data) - { - int pos = offset; + int stringByteCount; + if (in.remaining() >= dataLenAfterFormula + 3) { + int tag = in.readByte(); + if (tag != 0x03) { + throw new RecordFormatException("Expected byte 0x03 here"); + } + int nChars = in.readUShort(); + if (nChars > 0) { + // OOO: the 4th way Xcl stores a unicode string: not even a Grbit byte present if length 0 + field_3_unicode_flag = ( in.readByte() & 0x01 ) != 0; + if (field_3_unicode_flag) { + field_4_ole_classname = in.readUnicodeLEString(nChars); + stringByteCount = nChars * 2; + } else { + field_4_ole_classname = in.readCompressedUnicode(nChars); + stringByteCount = nChars; + } + } else { + field_4_ole_classname = ""; + stringByteCount = 0; + } + } else { + field_4_ole_classname = null; + stringByteCount = 0; + } + // Pad to next 2-byte boundary + if (((stringByteCount + formulaSize) % 2) != 0) { + int b = in.readByte(); + if (field_2_refPtg != null && field_4_ole_classname == null) { + field_4_unknownByte = new Byte((byte)b); + } + } + int nUnexpectedPadding = in.remaining() - dataLenAfterFormula; - LittleEndian.putShort(data, pos, sid); pos += 2; - LittleEndian.putShort(data, pos, (short)(getRecordSize() - 4)); pos += 2; + if (nUnexpectedPadding > 0) { + System.err.println("Discarding " + nUnexpectedPadding + " unexpected padding bytes "); + readRawData(in, nUnexpectedPadding); + } - LittleEndian.putShort(data, pos, field_1_stream_id_offset); pos += 2; - LittleEndian.putShortArray(data, pos, field_2_unknown); pos += field_2_unknown.length * 2 + 2; - LittleEndian.putShort(data, pos, field_3_unicode_len); pos += 2; - data[pos] = field_4_unicode_flag ? (byte) 0x01 : (byte) 0x00; pos++; + // Fetch the stream ID + if (dataLenAfterFormula >= 4) { + field_5_stream_id = new Integer(in.readInt()); + } else { + field_5_stream_id = null; + } - if ( field_4_unicode_flag ) - { - StringUtil.putUnicodeLE( field_5_ole_classname, data, pos ); pos += field_5_ole_classname.length() * 2; - } - else - { - StringUtil.putCompressedUnicode( field_5_ole_classname, data, pos ); pos += field_5_ole_classname.length(); - } + field_6_unknown = in.readRemainder(); + } - // Padded with the same number of NUL bytes as were originally skipped. - // XXX: This is only accurate until we make the classname mutable. - pos += field_5_ole_classname_padding; - - LittleEndian.putInt(data, pos, field_6_stream_id); pos += 4; + private static Ptg readRefPtg(byte[] formulaRawBytes) { + byte[] data = new byte[formulaRawBytes.length + 4]; + LittleEndian.putUShort(data, 0, -5555); + LittleEndian.putUShort(data, 2, formulaRawBytes.length); + System.arraycopy(formulaRawBytes, 0, data, 4, formulaRawBytes.length); + RecordInputStream in = new RecordInputStream(new ByteArrayInputStream(data)); + in.nextRecord(); + byte ptgSid = in.readByte(); + switch(ptgSid) { + case AreaPtg.sid: return new AreaPtg(in); + case Area3DPtg.sid: return new Area3DPtg(in); + case RefPtg.sid: return new RefPtg(in); + case Ref3DPtg.sid: return new Ref3DPtg(in); + } + return null; + } - System.arraycopy(remainingBytes, 0, data, pos, remainingBytes.length); + private static byte[] readRawData(RecordInputStream in, int size) { + if (size < 0) { + throw new IllegalArgumentException("Negative size (" + size + ")"); + } + if (size == 0) { + return EMPTY_BYTE_ARRAY; + } + byte[] result = new byte[size]; + for(int i=0; i< size; i++) { + result[i] = in.readByte(); + } + return result; + } + + private int getStreamIDOffset(int formulaSize) { + int result = 2 + 4; // formulaSize + f2unknown_int + result += formulaSize; + + int stringLen; + if (field_4_ole_classname == null) { + // don't write 0x03, stringLen, flag, text + stringLen = 0; + } else { + result += 1 + 2 + 1; // 0x03, stringLen, flag + stringLen = field_4_ole_classname.length(); + if (field_3_unicode_flag) { + result += stringLen * 2; + } else { + result += stringLen; + } + } + // pad to next 2 byte boundary + if ((result % 2) != 0) { + result ++; + } + return result; + } + + private int getDataSize(int idOffset) { - return getRecordSize(); - } + int result = 2 + idOffset; // 2 for idOffset short field itself + if (field_5_stream_id != null) { + result += 4; + } + return result + field_6_unknown.length; + } + private int getDataSize() { + int formulaSize = field_2_refPtg == null ? field_2_unknownFormulaData.length : field_2_refPtg.getSize(); + int idOffset = getStreamIDOffset(formulaSize); + return getDataSize(idOffset); + } - /** - * Size of record (exluding 4 byte header) - */ - public int getRecordSize() - { - // The stream id offset is relative to after the stream ID. - // Add 2 bytes for the stream id offset and 4 bytes for the stream id itself and 4 byts for the record header. - return remainingBytes.length + field_1_stream_id_offset + 2 + 4 + 4; - } + public int serialize(int base, byte[] data) { - /** - * Gets the stream ID containing the actual data. The data itself - * can be found under a top-level directory entry in the OLE2 filesystem - * under the name "MBDxxxxxxxx" where xxxxxxxx is - * this ID converted into hex (in big endian order, funnily enough.) - * - * @return the data stream ID. - */ - public int getStreamId() - { - return field_6_stream_id; - } + int formulaSize = field_2_refPtg == null ? field_2_unknownFormulaData.length : field_2_refPtg.getSize(); + int idOffset = getStreamIDOffset(formulaSize); + int dataSize = getDataSize(idOffset); + - public String toString() - { - StringBuffer buffer = new StringBuffer(); - buffer.append("[ftPictFmla]\n"); - buffer.append(" .streamIdOffset = ") - .append("0x").append(HexDump.toHex( field_1_stream_id_offset )) - .append(" (").append( field_1_stream_id_offset ).append(" )") - .append(System.getProperty("line.separator")); - buffer.append(" .unknown = ") - .append("0x").append(HexDump.toHex( field_2_unknown )) - .append(" (").append( field_2_unknown.length ).append(" )") - .append(System.getProperty("line.separator")); - buffer.append(" .unicodeLen = ") - .append("0x").append(HexDump.toHex( field_3_unicode_len )) - .append(" (").append( field_3_unicode_len ).append(" )") - .append(System.getProperty("line.separator")); - buffer.append(" .unicodeFlag = ") - .append("0x").append( field_4_unicode_flag ? 0x01 : 0x00 ) - .append(" (").append( field_4_unicode_flag ).append(" )") - .append(System.getProperty("line.separator")); - buffer.append(" .oleClassname = ") - .append(field_5_ole_classname) - .append(System.getProperty("line.separator")); - buffer.append(" .streamId = ") - .append("0x").append(HexDump.toHex( field_6_stream_id )) - .append(" (").append( field_6_stream_id ).append(" )") - .append(System.getProperty("line.separator")); - buffer.append("[/ftPictFmla]"); - return buffer.toString(); - } + LittleEndian.putUShort(data, base + 0, sid); + LittleEndian.putUShort(data, base + 2, dataSize); + LittleEndian.putUShort(data, base + 4, idOffset); + LittleEndian.putUShort(data, base + 6, formulaSize); + LittleEndian.putInt(data, base + 8, field_1_unknown_int); + + int pos = base+12; + + if (field_2_refPtg == null) { + System.arraycopy(field_2_unknownFormulaData, 0, data, pos, field_2_unknownFormulaData.length); + } else { + field_2_refPtg.writeBytes(data, pos); + } + pos += formulaSize; + + int stringLen; + if (field_4_ole_classname == null) { + // don't write 0x03, stringLen, flag, text + stringLen = 0; + } else { + LittleEndian.putByte(data, pos, 0x03); + pos += 1; + stringLen = field_4_ole_classname.length(); + LittleEndian.putUShort(data, pos, stringLen); + pos += 2; + LittleEndian.putByte(data, pos, field_3_unicode_flag ? 0x01 : 0x00); + pos += 1; + + if (field_3_unicode_flag) { + StringUtil.putUnicodeLE(field_4_ole_classname, data, pos); + pos += stringLen * 2; + } else { + StringUtil.putCompressedUnicode(field_4_ole_classname, data, pos); + pos += stringLen; + } + } + + // pad to next 2-byte boundary (requires 0 or 1 bytes) + switch(idOffset - (pos - 6 - base)) { // 6 for 3 shorts: sid, dataSize, idOffset + case 1: + LittleEndian.putByte(data, pos, field_4_unknownByte == null ? 0x00 : field_4_unknownByte.intValue()); + pos ++; + case 0: + break; + default: + throw new IllegalStateException("Bad padding calculation (" + idOffset + ", " + (pos-base) + ")"); + } + + if (field_5_stream_id != null) { + LittleEndian.putInt(data, pos, field_5_stream_id.intValue()); + pos += 4; + } + System.arraycopy(field_6_unknown, 0, data, pos, field_6_unknown.length); + + return 4 + dataSize; + } + + public int getRecordSize() { + return 4 + getDataSize(); + } + + /** + * Gets the stream ID containing the actual data. The data itself + * can be found under a top-level directory entry in the OLE2 filesystem + * under the name "MBDxxxxxxxx" where xxxxxxxx is + * this ID converted into hex (in big endian order, funnily enough.) + * + * @return the data stream ID. Possibly null + */ + public Integer getStreamId() { + return field_5_stream_id; + } + + public String getOLEClassName() { + return field_4_ole_classname; + } + + public byte[] getObjectData() { + return field_6_unknown; + } + + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("[ftPictFmla]\n"); + sb.append(" .f2unknown = ").append(HexDump.intToHex(field_1_unknown_int)).append("\n"); + if (field_2_refPtg == null) { + sb.append(" .f3unknown = ").append(HexDump.toHex(field_2_unknownFormulaData)).append("\n"); + } else { + sb.append(" .formula = ").append(field_2_refPtg.toString()).append("\n"); + } + if (field_4_ole_classname != null) { + sb.append(" .unicodeFlag = ").append(field_3_unicode_flag).append("\n"); + sb.append(" .oleClassname = ").append(field_4_ole_classname).append("\n"); + } + if (field_4_unknownByte != null) { + sb.append(" .f4unknown = ").append(HexDump.byteToHex(field_4_unknownByte.intValue())).append("\n"); + } + if (field_5_stream_id != null) { + sb.append(" .streamId = ").append(HexDump.intToHex(field_5_stream_id.intValue())).append("\n"); + } + if (field_6_unknown.length > 0) { + sb.append(" .f7unknown = ").append(HexDump.toHex(field_6_unknown)).append("\n"); + } + sb.append("[/ftPictFmla]"); + return sb.toString(); + } } diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index fe6a4b2ea3..c23868c711 100755 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -33,7 +33,7 @@ public class RecordInputStream extends InputStream { /** Maximum size of a single record (minus the 4 byte header) without a continue*/ public final static short MAX_RECORD_DATA_SIZE = 8224; private static final int INVALID_SID_VALUE = -1; - + private InputStream in; protected short currentSid; protected short currentLength = -1; @@ -42,34 +42,34 @@ public class RecordInputStream extends InputStream { protected byte[] data = new byte[MAX_RECORD_DATA_SIZE]; protected short recordOffset; protected long pos; - + private boolean autoContinue = true; - public RecordInputStream(InputStream in) throws RecordFormatException { + public RecordInputStream(InputStream in) throws RecordFormatException { this.in = in; try { nextSid = LittleEndian.readShort(in); - //Dont increment the pos just yet (technically we are at the start of - //the record stream until nextRecord is called). + //Don't increment the pos just yet (technically we are at the start of + //the record stream until nextRecord is called). } catch (IOException ex) { throw new RecordFormatException("Error reading bytes", ex); } } - - /** This method will read a byte from the current record*/ - public int read() { - checkRecordPosition(); - byte result = data[recordOffset]; - recordOffset += 1; - pos += 1; - return result; - } - + /** This method will read a byte from the current record*/ + public int read() { + checkRecordPosition(LittleEndian.BYTE_SIZE); + + byte result = data[recordOffset]; + recordOffset += LittleEndian.BYTE_SIZE; + pos += LittleEndian.BYTE_SIZE; + return result; + } + public short getSid() { return currentSid; } - + public short getLength() { return currentLength; } @@ -85,12 +85,11 @@ public class RecordInputStream extends InputStream { public boolean hasNextRecord() { return nextSid != INVALID_SID_VALUE; } - + /** Moves to the next record in the stream. - * + * * Note: The auto continue flag is reset to true */ - public void nextRecord() throws RecordFormatException { if ((currentLength != -1) && (currentLength != recordOffset)) { System.out.println("WARN. Unread "+remaining()+" bytes of record 0x"+Integer.toHexString(currentSid)); @@ -100,7 +99,7 @@ public class RecordInputStream extends InputStream { autoContinue = true; try { recordOffset = 0; - currentLength = LittleEndian.readShort(in); + currentLength = LittleEndian.readShort(in); if (currentLength > MAX_RECORD_DATA_SIZE) throw new RecordFormatException("The content of an excel record cannot exceed "+MAX_RECORD_DATA_SIZE+" bytes"); pos += LittleEndian.SHORT_SIZE; @@ -113,138 +112,124 @@ public class RecordInputStream extends InputStream { // ex45582-22397.xls has one extra byte after the last record // Excel reads that file OK } - nextSid = INVALID_SID_VALUE; + nextSid = INVALID_SID_VALUE; } else { nextSid = LittleEndian.readShort(in); if (nextSid == INVALID_SID_VALUE) { throw new RecordFormatException("Found sid " + nextSid + " after record with sid 0x" + Integer.toHexString(currentSid).toUpperCase()); } - } + } } catch (IOException ex) { throw new RecordFormatException("Error reading bytes", ex); } } - + public void setAutoContinue(boolean enable) { - this.autoContinue = enable; + this.autoContinue = enable; } - + public boolean getAutoContinue() { return autoContinue; } - - protected void checkRecordPosition() { - if (remaining() <= 0) { - if (isContinueNext() && autoContinue) { - nextRecord(); - } - else throw new ArrayIndexOutOfBoundsException(); - } - } - - /** - * Reads an 8 bit, signed value - */ - public byte readByte() { - checkRecordPosition(); - - byte result = data[recordOffset]; - recordOffset += 1; - pos += 1; - return result; - } - - /** - * Reads a 16 bit, signed value - */ - public short readShort() { - checkRecordPosition(); - - short result = LittleEndian.getShort(data, recordOffset); - recordOffset += LittleEndian.SHORT_SIZE; - pos += LittleEndian.SHORT_SIZE; - return result; - } - public int readInt() { - checkRecordPosition(); - - int result = LittleEndian.getInt(data, recordOffset); - recordOffset += LittleEndian.INT_SIZE; - pos += LittleEndian.INT_SIZE; - return result; - } + private void checkRecordPosition(int requiredByteCount) { - public long readLong() { - checkRecordPosition(); - - long result = LittleEndian.getLong(data, recordOffset); - recordOffset += LittleEndian.LONG_SIZE; - pos += LittleEndian.LONG_SIZE; - return result; - } + if (remaining() < requiredByteCount) { + if (isContinueNext() && autoContinue) { + nextRecord(); + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } + } + + /** + * Reads an 8 bit, signed value + */ + public byte readByte() { + checkRecordPosition(LittleEndian.BYTE_SIZE); + + byte result = data[recordOffset]; + recordOffset += LittleEndian.BYTE_SIZE; + pos += LittleEndian.BYTE_SIZE; + return result; + } + + /** + * Reads a 16 bit, signed value + */ + public short readShort() { + checkRecordPosition(LittleEndian.SHORT_SIZE); + + short result = LittleEndian.getShort(data, recordOffset); + recordOffset += LittleEndian.SHORT_SIZE; + pos += LittleEndian.SHORT_SIZE; + return result; + } + + public int readInt() { + checkRecordPosition(LittleEndian.INT_SIZE); + + int result = LittleEndian.getInt(data, recordOffset); + recordOffset += LittleEndian.INT_SIZE; + pos += LittleEndian.INT_SIZE; + return result; + } + + public long readLong() { + checkRecordPosition(LittleEndian.LONG_SIZE); + + long result = LittleEndian.getLong(data, recordOffset); + recordOffset += LittleEndian.LONG_SIZE; + pos += LittleEndian.LONG_SIZE; + return result; + } + + /** + * Reads an 8 bit, unsigned value + */ + public short readUByte() { + return (short) (readByte() & 0x00FF); + } + + /** + * Reads a 16 bit, unsigned value. + * @return + */ + public int readUShort() { + checkRecordPosition(LittleEndian.SHORT_SIZE); + + int result = LittleEndian.getUShort(data, recordOffset); + recordOffset += LittleEndian.SHORT_SIZE; + pos += LittleEndian.SHORT_SIZE; + return result; + } + + public double readDouble() { + checkRecordPosition(LittleEndian.DOUBLE_SIZE); + long valueLongBits = LittleEndian.getLong(data, recordOffset); + double result = Double.longBitsToDouble(valueLongBits); + if (Double.isNaN(result)) { + throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN + } + recordOffset += LittleEndian.DOUBLE_SIZE; + pos += LittleEndian.DOUBLE_SIZE; + return result; + } /** - * Reads an 8 bit, unsigned value - */ - public short readUByte() { - short s = readByte(); - if(s < 0) { - s += 256; - } - return s; - } - - /** - * Reads a 16 bit,un- signed value. - * @return - */ - public int readUShort() { - checkRecordPosition(); - - int result = LittleEndian.getUShort(data, recordOffset); - recordOffset += LittleEndian.SHORT_SIZE; - pos += LittleEndian.SHORT_SIZE; - return result; - } - - public double readDouble() { - checkRecordPosition(); - long valueLongBits = LittleEndian.getLong(data, recordOffset); - double result = Double.longBitsToDouble(valueLongBits); - if (Double.isNaN(result)) { - throw new RuntimeException("Did not expect to read NaN"); - } - recordOffset += LittleEndian.DOUBLE_SIZE; - pos += LittleEndian.DOUBLE_SIZE; - return result; - } - - - public short[] readShortArray() { - checkRecordPosition(); - - short[] arr = LittleEndian.getShortArray(data, recordOffset); - final int size = (2 * (arr.length +1)); - recordOffset += size; - pos += size; - - return arr; - } - - /** - * given a byte array of 16-bit unicode characters, compress to 8-bit and - * return a string - * - * { 0x16, 0x00 } -0x16 - * + * given a byte array of 16-bit unicode characters, compress to 8-bit and + * return a string + * + * { 0x16, 0x00 } -0x16 + * * @param length the length of the final string * @return the converted string * @exception IllegalArgumentException if len is too large (i.e., - * there is not enough data in string to create a String of that - * length) - */ + * there is not enough data in string to create a String of that + * length) + */ public String readUnicodeLEString(int length) { if ((length < 0) || (((remaining() / 2) < length) && !isContinueNext())) { throw new IllegalArgumentException("Illegal length - asked for " + length + " but only " + (remaining()/2) + " left!"); @@ -258,11 +243,11 @@ public class RecordInputStream extends InputStream { if(compressByte != 1) throw new IllegalArgumentException("compressByte in continue records must be 1 while reading unicode LE string"); } char ch = (char)readShort(); - buf.append(ch); + buf.append(ch); } return buf.toString(); } - + public String readCompressedUnicode(int length) { if ((length < 0) || ((remaining() < length) && !isContinueNext())) { throw new IllegalArgumentException("Illegal length " + length); @@ -277,23 +262,23 @@ public class RecordInputStream extends InputStream { } byte b = readByte(); char ch = (char)(0x00FF & b); // avoid sex - buf.append(ch); + buf.append(ch); } - return buf.toString(); + return buf.toString(); } - + /** Returns an excel style unicode string from the bytes reminaing in the record. * Note: Unicode strings differ from normal strings due to the addition of * formatting information. - * + * * @return The unicode string representation of the remaining bytes. */ public UnicodeString readUnicodeString() { return new UnicodeString(this); } - + /** Returns the remaining bytes for the current record. - * + * * @return The remaining bytes of the current record. */ public byte[] readRemainder() { @@ -304,39 +289,39 @@ public class RecordInputStream extends InputStream { pos += size; return result; } - + /** Reads all byte data for the current record, including any * that overlaps into any following continue records. - * + * * @deprecated Best to write a input stream that wraps this one where there is * special sub record that may overlap continue records. - */ + */ public byte[] readAllContinuedRemainder() { //Using a ByteArrayOutputStream is just an easy way to get a //growable array of the data. ByteArrayOutputStream out = new ByteArrayOutputStream(2*MAX_RECORD_DATA_SIZE); while (isContinueNext()) { - byte[] b = readRemainder(); + byte[] b = readRemainder(); out.write(b, 0, b.length); nextRecord(); } - byte[] b = readRemainder(); - out.write(b, 0, b.length); - + byte[] b = readRemainder(); + out.write(b, 0, b.length); + return out.toByteArray(); } /** The remaining number of bytes in the current record. - * + * * @return The number of bytes remaining in the current record */ public int remaining() { return (currentLength - recordOffset); } - /** Returns true iif a Continue record is next in the excel stream - * + /** Returns true iif a Continue record is next in the excel stream + * * @return True when a ContinueRecord is next. */ public boolean isContinueNext() { diff --git a/src/java/org/apache/poi/hssf/record/SeriesListRecord.java b/src/java/org/apache/poi/hssf/record/SeriesListRecord.java index 2894f15877..4753eae05f 100644 --- a/src/java/org/apache/poi/hssf/record/SeriesListRecord.java +++ b/src/java/org/apache/poi/hssf/record/SeriesListRecord.java @@ -1,4 +1,3 @@ - /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,66 +14,66 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.record; - - -import org.apache.poi.util.*; +import org.apache.poi.util.LittleEndian; /** - * The series list record defines the series displayed as an overlay to the main chart record. - * NOTE: This source is automatically generated please do not modify this file. Either subclass or - * remove the record in src/records/definitions. - + * + * The series list record defines the series displayed as an overlay to the main chart record.
+ * TODO - does this record (0x1016) really exist. It doesn't seem to be referenced in either the OOO or MS doc + * * @author Glen Stampoultzis (glens at apache.org) */ -public class SeriesListRecord - extends Record -{ - public final static short sid = 0x1016; +public final class SeriesListRecord extends Record { + public final static short sid = 0x1016; private short[] field_1_seriesNumbers; - - public SeriesListRecord() - { - + public SeriesListRecord(short[] seriesNumbers) { + field_1_seriesNumbers = seriesNumbers; } - public SeriesListRecord(RecordInputStream in) - { - field_1_seriesNumbers = in.readShortArray(); + public SeriesListRecord(RecordInputStream in) { + int nItems = in.readUShort(); + short[] ss = new short[nItems]; + for (int i = 0; i < nItems; i++) { + ss[i] = in.readShort(); + + } + field_1_seriesNumbers = ss; } - public String toString() - { + public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("[SERIESLIST]\n"); - buffer.append(" .seriesNumbers = ") - .append(" (").append( getSeriesNumbers() ).append(" )"); - buffer.append(System.getProperty("line.separator")); + buffer.append(" .seriesNumbers= ").append(" (").append( getSeriesNumbers() ).append(" )"); + buffer.append("\n"); buffer.append("[/SERIESLIST]\n"); return buffer.toString(); } - public int serialize(int offset, byte[] data) - { - int pos = 0; + public int serialize(int offset, byte[] data) { - LittleEndian.putShort(data, 0 + offset, sid); - LittleEndian.putShort(data, 2 + offset, (short)(getRecordSize() - 4)); + int nItems = field_1_seriesNumbers.length; + int dataSize = 2 + 2 * nItems; + + LittleEndian.putUShort(data, 0 + offset, sid); + LittleEndian.putUShort(data, 2 + offset, dataSize); - LittleEndian.putShortArray(data, 4 + offset + pos, field_1_seriesNumbers); + LittleEndian.putUShort(data, 4 + offset, nItems); + + int pos = offset + 6; + for (int i = 0; i < nItems; i++) { + LittleEndian.putUShort(data, pos, field_1_seriesNumbers[i]); + pos += 2; + } - return getRecordSize(); + return 4 + dataSize; } - /** - * Size of record (exluding 4 byte header) - */ public int getRecordSize() { return 4 + field_1_seriesNumbers.length * 2 + 2; @@ -86,34 +85,23 @@ public class SeriesListRecord } public Object clone() { - SeriesListRecord rec = new SeriesListRecord(); - - rec.field_1_seriesNumbers = field_1_seriesNumbers; - return rec; + return new SeriesListRecord((short[]) field_1_seriesNumbers.clone()); } - - - /** * Get the series numbers field for the SeriesList record. */ - public short[] getSeriesNumbers() - { + public short[] getSeriesNumbers() { return field_1_seriesNumbers; } /** * Set the series numbers field for the SeriesList record. */ - public void setSeriesNumbers(short[] field_1_seriesNumbers) - { + public void setSeriesNumbers(short[] field_1_seriesNumbers) { this.field_1_seriesNumbers = field_1_seriesNumbers; } - - -} // END OF CLASS - +} diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFObjectData.java b/src/java/org/apache/poi/hssf/usermodel/HSSFObjectData.java index 697c33b9e2..1db5883303 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFObjectData.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFObjectData.java @@ -32,7 +32,7 @@ import org.apache.poi.util.HexDump; * * @author Daniel Noll */ -public class HSSFObjectData +public final class HSSFObjectData { /** * Underlying object record ultimately containing a reference to the object. @@ -60,8 +60,7 @@ public class HSSFObjectData * Returns the OLE2 Class Name of the object */ public String getOLE2ClassName() { - EmbeddedObjectRefSubRecord subRecord = findObjectRecord(); - return subRecord.field_5_ole_classname; + return findObjectRecord().getOLEClassName(); } /** @@ -72,9 +71,9 @@ public class HSSFObjectData * @throws IOException if there was an error reading the data. */ public DirectoryEntry getDirectory() throws IOException { - EmbeddedObjectRefSubRecord subRecord = findObjectRecord(); + EmbeddedObjectRefSubRecord subRecord = findObjectRecord(); - int streamId = ((EmbeddedObjectRefSubRecord) subRecord).getStreamId(); + int streamId = subRecord.getStreamId().intValue(); String streamName = "MBD" + HexDump.toHex(streamId); Entry entry = poifs.getRoot().getEntry(streamName); @@ -91,8 +90,7 @@ public class HSSFObjectData * Entry */ public byte[] getObjectData() { - EmbeddedObjectRefSubRecord subRecord = findObjectRecord(); - return subRecord.remainingBytes; + return findObjectRecord().getObjectData(); } /** @@ -101,10 +99,11 @@ public class HSSFObjectData * (Not all do, those that don't have a data portion) */ public boolean hasDirectoryEntry() { - EmbeddedObjectRefSubRecord subRecord = findObjectRecord(); - - // Field 6 tells you - return (subRecord.field_6_stream_id != 0); + EmbeddedObjectRefSubRecord subRecord = findObjectRecord(); + + // 'stream id' field tells you + Integer streamId = subRecord.getStreamId(); + return streamId != null && streamId.intValue() != 0; } /** @@ -117,7 +116,7 @@ public class HSSFObjectData while (subRecordIter.hasNext()) { Object subRecord = subRecordIter.next(); if (subRecord instanceof EmbeddedObjectRefSubRecord) { - return (EmbeddedObjectRefSubRecord)subRecord; + return (EmbeddedObjectRefSubRecord)subRecord; } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java index aacf663863..24ef987eee 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java @@ -724,8 +724,8 @@ public class HSSFWorkbook extends POIDocument implements org.apache.poi.ss.userm HSSFName newName = new HSSFName(this, newNameRecord); names.add(newName); + workbook.cloneDrawings(clonedSheet.getSheet()); } - workbook.cloneDrawings(clonedSheet.getSheet()); // TODO - maybe same logic required for other/all built-in name records return clonedSheet; diff --git a/src/java/org/apache/poi/ss/formula/CellCacheEntry.java b/src/java/org/apache/poi/ss/formula/CellCacheEntry.java index 861874c25b..bd7500023c 100644 --- a/src/java/org/apache/poi/ss/formula/CellCacheEntry.java +++ b/src/java/org/apache/poi/ss/formula/CellCacheEntry.java @@ -20,6 +20,7 @@ package org.apache.poi.ss.formula; import java.util.HashSet; import java.util.Set; +import org.apache.poi.hssf.record.formula.eval.BlankEval; import org.apache.poi.hssf.record.formula.eval.BoolEval; import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.NumberEval; @@ -60,6 +61,9 @@ final class CellCacheEntry { // value type is changing return false; } + if (a == BlankEval.INSTANCE) { + return b == a; + } if (cls == NumberEval.class) { return ((NumberEval)a).getNumberValue() == ((NumberEval)b).getNumberValue(); } diff --git a/src/java/org/apache/poi/ss/formula/CellLocation.java b/src/java/org/apache/poi/ss/formula/CellLocation.java index 6857c4bc1b..bb32e3dfa3 100644 --- a/src/java/org/apache/poi/ss/formula/CellLocation.java +++ b/src/java/org/apache/poi/ss/formula/CellLocation.java @@ -25,24 +25,24 @@ import org.apache.poi.hssf.util.CellReference; final class CellLocation { public static final CellLocation[] EMPTY_ARRAY = { }; - private final EvaluationWorkbook _book; + private final int _bookIx; private final int _sheetIndex; private final int _rowIndex; private final int _columnIndex; private final int _hashCode; - public CellLocation(EvaluationWorkbook book, int sheetIndex, int rowIndex, int columnIndex) { + public CellLocation(int bookIx, int sheetIndex, int rowIndex, int columnIndex) { if (sheetIndex < 0) { throw new IllegalArgumentException("sheetIndex must not be negative"); } - _book = book; + _bookIx = bookIx; _sheetIndex = sheetIndex; _rowIndex = rowIndex; _columnIndex = columnIndex; - _hashCode = System.identityHashCode(book) + sheetIndex + 17 * (rowIndex + 17 * columnIndex); + _hashCode = _bookIx + 17 * (sheetIndex + 17 * (rowIndex + 17 * columnIndex)); } - public Object getBook() { - return _book; + public int getBookIndex() { + return _bookIx; } public int getSheetIndex() { return _sheetIndex; @@ -65,7 +65,7 @@ final class CellLocation { if (getSheetIndex() != other.getSheetIndex()) { return false; } - if (getBook() != other.getBook()) { + if (getBookIndex() != other.getBookIndex()) { return false; } return true; diff --git a/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java b/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java index c62d2f182d..7939596d3b 100644 --- a/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java +++ b/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java @@ -97,7 +97,7 @@ public final class CollaboratingWorkbooksEnvironment { EvaluationCache cache = new EvaluationCache(evalListener); for(int i=0; i