diff --git a/src/java/org/apache/poi/hssf/record/HyperlinkRecord.java b/src/java/org/apache/poi/hssf/record/HyperlinkRecord.java
index 04697a52d..38f985506 100644
--- a/src/java/org/apache/poi/hssf/record/HyperlinkRecord.java
+++ b/src/java/org/apache/poi/hssf/record/HyperlinkRecord.java
@@ -17,11 +17,17 @@
package org.apache.poi.hssf.record;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
import java.io.IOException;
-import java.util.Arrays;
-import org.apache.poi.util.LittleEndian;
-import org.apache.poi.util.StringUtil;
+
+import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.util.HexDump;
+import org.apache.poi.util.HexRead;
+import org.apache.poi.util.LittleEndianByteArrayInputStream;
+import org.apache.poi.util.LittleEndianInput;
+import org.apache.poi.util.LittleEndianOutput;
+import org.apache.poi.util.StringUtil;
/**
* The HyperlinkRecord
(0x01B8) wraps an HLINK-record
@@ -31,97 +37,210 @@ import org.apache.poi.util.HexDump;
* @author Mark Hissink Muller
+ * 13579BDF-0246-8ACE-0123-456789ABCDEF
+ *
->
+ * 0x13579BDF, 0x0246, 0x8ACE 0x0123456789ABCDEF
+ */
+ public static GUID parse(String rep) {
+ char[] cc = rep.toCharArray();
+ if (cc.length != TEXT_FORMAT_LENGTH) {
+ throw new RecordFormatException("supplied text is the wrong length for a GUID");
+ }
+ int d0 = (parseShort(cc, 0) << 16) + (parseShort(cc, 4) << 0);
+ int d1 = parseShort(cc, 9);
+ int d2 = parseShort(cc, 14);
+ for (int i = 23; i > 19; i--) {
+ cc[i] = cc[i - 1];
+ }
+ long d3 = parseLELong(cc, 20);
+
+ return new GUID(d0, d1, d2, d3);
+ }
+
+ private static long parseLELong(char[] cc, int startIndex) {
+ long acc = 0;
+ for (int i = startIndex + 14; i >= startIndex; i -= 2) {
+ acc <<= 4;
+ acc += parseHexChar(cc[i + 0]);
+ acc <<= 4;
+ acc += parseHexChar(cc[i + 1]);
+ }
+ return acc;
+ }
+
+ private static int parseShort(char[] cc, int startIndex) {
+ int acc = 0;
+ for (int i = 0; i < 4; i++) {
+ acc <<= 4;
+ acc += parseHexChar(cc[startIndex + i]);
+ }
+ return acc;
+ }
+
+ private static int parseHexChar(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ throw new RecordFormatException("Bad hex char '" + c + "'");
+ }
+ }
+
/**
* Link flags
*/
- protected static final int HLINK_URL = 0x01; // File link or URL.
- protected static final int HLINK_ABS = 0x02; // Absolute path.
- protected static final int HLINK_LABEL = 0x14; // Has label.
- protected static final int HLINK_PLACE = 0x08; // Place in worksheet.
+ static final int HLINK_URL = 0x01; // File link or URL.
+ static final int HLINK_ABS = 0x02; // Absolute path.
+ static final int HLINK_LABEL = 0x14; // Has label/description.
+ /** Place in worksheet. If set, the {@link #_textMark} field will be present */
+ static final int HLINK_PLACE = 0x08;
+ private static final int HLINK_TARGET_FRAME = 0x80; // has 'target frame'
+ private static final int HLINK_UNC_PATH = 0x100; // has UNC path
+ final static GUID STD_MONIKER = GUID.parse("79EAC9D0-BAF9-11CE-8C82-00AA004BA90B");
+ final static GUID URL_MONIKER = GUID.parse("79EAC9E0-BAF9-11CE-8C82-00AA004BA90B");
+ final static GUID FILE_MONIKER = GUID.parse("00000303-0000-0000-C000-000000000046");
+ /** expected Tail of a URL link */
+ private final static byte[] URL_TAIL = HexRead.readFromString("79 58 81 F4 3B 1D 7F 48 AF 2C 82 5D C4 85 27 63 00 00 00 00 A5 AB 00 00");
+ /** expected Tail of a file link */
+ private final static byte[] FILE_TAIL = HexRead.readFromString("FF FF AD DE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00");
- protected final static byte[] STD_MONIKER = {(byte)0xD0, (byte)0xC9, (byte)0xEA, 0x79, (byte)0xF9, (byte)0xBA, (byte)0xCE, 0x11,
- (byte)0x8C, (byte)0x82, 0x00, (byte)0xAA, 0x00, 0x4B, (byte)0xA9, 0x0B };
- protected final static byte[] URL_MONIKER = {(byte)0xE0, (byte)0xC9, (byte)0xEA, 0x79, (byte)0xF9, (byte)0xBA, (byte)0xCE, 0x11,
- (byte)0x8C, (byte)0x82, 0x00, (byte)0xAA, 0x00, 0x4B, (byte)0xA9, 0x0B };
- protected final static byte[] FILE_MONIKER = {0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte)0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46};
+ private static final int TAIL_SIZE = FILE_TAIL.length;
+ /** cell range of this hyperlink */
+ private CellRangeAddress _range;
+
+ /** 16-byte GUID */
+ private GUID _guid;
+ /** Some sort of options for file links. */
+ private int _fileOpts;
+ /** Link options. Can include any of HLINK_* flags. */
+ private int _linkOpts;
+ /** Test label */
+ private String _label;
+
+ private String _targetFrame;
+ /** Moniker. Makes sense only for URL and file links */
+ private GUID _moniker;
+ /** in 8:3 DOS format No Unicode string header,
+ * always 8-bit characters, zero-terminated */
+ private String _shortFilename;
+ /** Link */
+ private String _address;
/**
- * Tail of a URL link
+ * Text describing a place in document. In Excel UI, this is appended to the
+ * address, (after a '#' delimiter).
+ * This field is optional. If present, the {@link #HLINK_PLACE} must be set.
*/
- protected final static byte[] URL_TAIL = {0x79, 0x58, (byte)0x81, (byte)0xF4, 0x3B, 0x1D, 0x7F, 0x48, (byte)0xAF, 0x2C,
- (byte)0x82, 0x5D, (byte)0xC4, (byte)0x85, 0x27, 0x63, 0x00, 0x00, 0x00,
- 0x00, (byte)0xA5, (byte)0xAB, 0x00, 0x00};
-
- /**
- * Tail of a file link
- */
- protected final static byte[] FILE_TAIL = {(byte)0xFF, (byte)0xFF, (byte)0xAD, (byte)0xDE, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-
- /**
- * First row of the hyperlink
- */
- private int rwFirst;
-
- /**
- * Last row of the hyperlink
- */
- private int rwLast;
-
- /**
- * First column of the hyperlink
- */
- private short colFirst;
-
- /**
- * Last column of the hyperlink
- */
- private short colLast;
-
- /**
- * 16-byte GUID
- */
- private byte[] guid;
-
- /**
- * Some sort of options. Seems to always equal 2
- */
- private int label_opts;
-
- /**
- * Some sort of options for file links.
- */
- private short file_opts;
-
- /**
- * Link options. Can include any of HLINK_* flags.
- */
- private int link_opts;
-
- /**
- * Test label
- */
- private String label;
-
- /**
- * Moniker. Makes sense only for URL and file links
- */
- private byte[] moniker;
-
- /**
- * Link
- */
- private String address;
-
- /**
- * Remaining bytes
- */
- private byte[] tail;
+ private String _textMark;
+
+ private byte[] _uninterpretedTail;
/**
* Create a new hyperlink
@@ -132,117 +251,100 @@ public final class HyperlinkRecord extends Record {
}
/**
- * Return the column of the first cell that contains the hyperlink
- *
- * @return the 0-based column of the first cell that contains the hyperlink
+ * @return the 0-based column of the first cell that contains this hyperlink
*/
- public short getFirstColumn()
- {
- return colFirst;
+ public int getFirstColumn() {
+ return _range.getFirstColumn();
}
/**
- * Set the column of the first cell that contains the hyperlink
- *
- * @param col the 0-based column of the first cell that contains the hyperlink
+ * Set the first column (zero-based)of the range that contains this hyperlink
*/
- public void setFirstColumn(short col)
- {
- this.colFirst = col;
+ public void setFirstColumn(int col) {
+ _range.setFirstColumn(col);
}
/**
- * Set the column of the last cell that contains the hyperlink
- *
- * @return the 0-based column of the last cell that contains the hyperlink
- */
- public short getLastColumn()
- {
- return colLast;
- }
-
- /**
- * Set the column of the last cell that contains the hyperlink
- *
- * @param col the 0-based column of the last cell that contains the hyperlink
+ * @return the 0-based column of the last cell that contains this hyperlink
*/
- public void setLastColumn(short col)
- {
- this.colLast = col;
+ public int getLastColumn() {
+ return _range.getLastColumn();
}
/**
- * Return the row of the first cell that contains the hyperlink
- *
- * @return the 0-based row of the first cell that contains the hyperlink
+ * Set the last column (zero-based)of the range that contains this hyperlink
*/
- public int getFirstRow()
- {
- return rwFirst;
+ public void setLastColumn(int col) {
+ _range.setLastColumn(col);
}
/**
- * Set the row of the first cell that contains the hyperlink
- *
- * @param row the 0-based row of the first cell that contains the hyperlink
+ * @return the 0-based row of the first cell that contains this hyperlink
*/
- public void setFirstRow(int row)
- {
- this.rwFirst = row;
+ public int getFirstRow() {
+ return _range.getFirstRow();
}
/**
- * Return the row of the last cell that contains the hyperlink
- *
- * @return the 0-based row of the last cell that contains the hyperlink
+ * Set the first row (zero-based)of the range that contains this hyperlink
*/
- public int getLastRow()
- {
- return rwLast;
+ public void setFirstRow(int col) {
+ _range.setFirstRow(col);
}
/**
- * Set the row of the last cell that contains the hyperlink
- *
- * @param row the 0-based row of the last cell that contains the hyperlink
+ * @return the 0-based row of the last cell that contains this hyperlink
*/
- public void setLastRow(int row)
- {
- this.rwLast = row;
+ public int getLastRow() {
+ return _range.getLastRow();
}
/**
- * Returns a 16-byte guid identifier. Seems to always equal {@link #STD_MONIKER}
- *
- * @return 16-byte guid identifier
+ * Set the last row (zero-based)of the range that contains this hyperlink
*/
- public byte[] getGuid()
- {
- return guid;
+ public void setLastRow(int col) {
+ _range.setLastRow(col);
+ }
+
+ /**
+ * @return 16-byte guid identifier Seems to always equal {@link #STD_MONIKER}
+ */
+ GUID getGuid() {
+ return _guid;
}
/**
- * Returns a 16-byte moniker.
- *
* @return 16-byte moniker
*/
- public byte[] getMoniker()
+ GUID getMoniker()
{
- return moniker;
+ return _moniker;
}
+ private static String cleanString(String s) {
+ if (s == null) {
+ return null;
+ }
+ int idx = s.indexOf('\u0000');
+ if (idx < 0) {
+ return s;
+ }
+ return s.substring(0, idx);
+ }
+ private static String appendNullTerm(String s) {
+ if (s == null) {
+ return null;
+ }
+ return s + '\u0000';
+ }
/**
* Return text label for this hyperlink
*
* @return text to display
*/
- public String getLabel()
- {
- if(label == null) return null;
-
- int idx = label.indexOf('\u0000');
- return idx == -1 ? label : label.substring(0, idx);
+ public String getLabel() {
+ return cleanString(_label);
}
/**
@@ -250,207 +352,289 @@ public final class HyperlinkRecord extends Record {
*
* @param label text label for this hyperlink
*/
- public void setLabel(String label)
- {
- this.label = label + '\u0000';
+ public void setLabel(String label) {
+ _label = appendNullTerm(label);
+ }
+ public String getTargetFrame() {
+ return cleanString(_targetFrame);
}
/**
- * Hypelink address. Depending on the hyperlink type it can be URL, e-mail, patrh to a file, etc.
+ * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, patrh to a file, etc.
*
* @return the address of this hyperlink
*/
- public String getAddress()
- {
- if(address == null) return null;
-
- int idx = address.indexOf('\u0000');
- return idx == -1 ? address : address.substring(0, idx);
+ public String getAddress() {
+ return cleanString(_address);
}
/**
- * Hypelink address. Depending on the hyperlink type it can be URL, e-mail, patrh to a file, etc.
+ * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, patrh to a file, etc.
*
* @param address the address of this hyperlink
*/
- public void setAddress(String address)
- {
- this.address = address + '\u0000';
+ public void setAddress(String address) {
+ _address = appendNullTerm(address);
}
+ public String getShortFilename() {
+ return cleanString(_shortFilename);
+ }
+
+ public void setShortFilename(String shortFilename) {
+ _shortFilename = appendNullTerm(shortFilename);
+ }
+
+ public String getTextMark() {
+ return cleanString(_textMark);
+ }
+ public void setTextMark(String textMark) {
+ _textMark = appendNullTerm(textMark);
+ }
+
+
/**
* Link options. Must be a combination of HLINK_* constants.
+ * For testing only
*/
- public int getLinkOptions(){
- return link_opts;
+ int getLinkOptions(){
+ return _linkOpts;
}
/**
* Label options
*/
public int getLabelOptions(){
- return label_opts;
+ return 2; // always 2
}
/**
* Options for a file link
*/
public int getFileOptions(){
- return file_opts;
+ return _fileOpts;
}
- public byte[] getTail(){
- return tail;
- }
- /**
- * @param in the RecordInputstream to read the record from
- */
- public HyperlinkRecord(RecordInputStream in)
- {
- try {
- rwFirst = in.readShort();
- rwLast = in.readUShort();
- colFirst = in.readShort();
- colLast = in.readShort();
+ public HyperlinkRecord(RecordInputStream in) {
+ _range = new CellRangeAddress(in);
- // 16-byte GUID
- guid = new byte[16];
- in.read(guid);
+ _guid = new GUID(in);
- label_opts = in.readInt();
- link_opts = in.readInt();
+ if (in.readInt() != 0x00000002) {
+ throw new RecordFormatException("expected "); // TODO
+ }
+ _linkOpts = in.readInt();
- if ((link_opts & HLINK_LABEL) != 0){
- int label_len = in.readInt();
- label = in.readUnicodeLEString(label_len);
- }
+ if ((_linkOpts & HLINK_LABEL) != 0){
+ int label_len = in.readInt();
+ _label = in.readUnicodeLEString(label_len);
+ }
- if ((link_opts & HLINK_URL) != 0){
- moniker = new byte[16];
- in.read(moniker);
+ if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
+ int len = in.readInt();
+ _targetFrame = in.readUnicodeLEString(len);
+ }
- if(Arrays.equals(URL_MONIKER, moniker)){
- int len = in.readInt();
+ if ((_linkOpts & HLINK_UNC_PATH) != 0) {
+ _moniker = null;
+ int nChars = in.readInt();
+ _address = in.readUnicodeLEString(nChars);
- address = in.readUnicodeLEString(len/2);
+ } else if ((_linkOpts & HLINK_URL) != 0) {
+ _moniker = new GUID(in);
- tail = in.readRemainder();
- } else if (Arrays.equals(FILE_MONIKER, moniker)){
- file_opts = in.readShort();
+ if(URL_MONIKER.equals(_moniker)){
+ int fieldSize = in.readInt();
- int len = in.readInt();
-
- byte[] path_bytes = new byte[len];
- in.read(path_bytes);
-
- address = new String(path_bytes);
-
- tail = in.readRemainder();
+ if ((_linkOpts & HLINK_TARGET_FRAME) != 0) {
+ int nChars = fieldSize/2;
+ _address = in.readUnicodeLEString(nChars);
+ } else {
+ int nChars = (fieldSize - TAIL_SIZE)/2;
+ _address = in.readUnicodeLEString(nChars);
+ _uninterpretedTail = readTail(URL_TAIL, in);
}
- } else if((link_opts & HLINK_PLACE) != 0){
+ } else if (FILE_MONIKER.equals(_moniker)) {
+ _fileOpts = in.readShort();
+
int len = in.readInt();
- address = in.readUnicodeLEString(len);
+ _shortFilename = StringUtil.readCompressedUnicode(in, len);
+ _uninterpretedTail = readTail(FILE_TAIL, in);
+ int size = in.readInt();
+ if (size > 0) {
+ int charDataSize = in.readInt();
+ if (in.readUShort() != 0x0003) {
+ throw new RecordFormatException("expected "); // TODO
+ }
+ _address = StringUtil.readUnicodeLE(in, charDataSize/2);
+ } else {
+ _address = null; // TODO
+ }
+ } else if (STD_MONIKER.equals(_moniker)) {
+ _fileOpts = in.readShort();
+
+ int len = in.readInt();
+
+ byte[] path_bytes = new byte[len];
+ in.readFully(path_bytes);
+
+ _address = new String(path_bytes);
}
- } catch (IOException e){
- throw new RuntimeException(e);
+ } else {
+ // can happen for a plain link within the current document
+ _moniker = null;
}
+ if((_linkOpts & HLINK_PLACE) != 0) {
+
+ int len = in.readInt();
+ _textMark = in.readUnicodeLEString(len);
+ }
+
+ if (in.remaining() > 0) {
+ System.out.println(HexDump.toHex(in.readRemainder()));
+ }
}
- public short getSid()
- {
- return HyperlinkRecord.sid;
- }
+ public void serialize(LittleEndianOutput out) {
+ _range.serialize(out);
- public int serialize(int offset, byte[] data)
- {
- int pos = offset;
- LittleEndian.putShort(data, pos, sid); pos += 2;
- LittleEndian.putShort(data, pos, ( short )(getRecordSize()-4)); pos += 2;
- LittleEndian.putUShort(data, pos, rwFirst); pos += 2;
- LittleEndian.putUShort(data, pos, rwLast); pos += 2;
- LittleEndian.putShort(data, pos, colFirst); pos += 2;
- LittleEndian.putShort(data, pos, colLast); pos += 2;
+ _guid.serialize(out);
+ out.writeInt(0x00000002); // TODO const
+ out.writeInt(_linkOpts);
- System.arraycopy(guid, 0, data, pos, guid.length); pos += guid.length;
-
- LittleEndian.putInt(data, pos, label_opts); pos += 4;
- LittleEndian.putInt(data, pos, link_opts); pos += 4;
-
- if ((link_opts & HLINK_LABEL) != 0){
- LittleEndian.putInt(data, pos, label.length()); pos += 4;
- StringUtil.putUnicodeLE(label, data, pos); pos += label.length()*2;
+ if ((_linkOpts & HLINK_LABEL) != 0){
+ out.writeInt(_label.length());
+ StringUtil.putUnicodeLE(_label, out);
}
- if ((link_opts & HLINK_URL) != 0){
- System.arraycopy(moniker, 0, data, pos, moniker.length); pos += moniker.length;
- if(Arrays.equals(URL_MONIKER, moniker)){
- LittleEndian.putInt(data, pos, address.length()*2 + tail.length); pos += 4;
- StringUtil.putUnicodeLE(address, data, pos); pos += address.length()*2;
- if(tail.length > 0){
- System.arraycopy(tail, 0, data, pos, tail.length); pos += tail.length;
+ if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
+ out.writeInt(_targetFrame.length());
+ StringUtil.putUnicodeLE(_targetFrame, out);
+ }
+
+ if ((_linkOpts & HLINK_UNC_PATH) != 0) {
+ out.writeInt(_address.length());
+ StringUtil.putUnicodeLE(_address, out);
+ } else if ((_linkOpts & HLINK_URL) != 0){
+ _moniker.serialize(out);
+ if(URL_MONIKER.equals(_moniker)){
+ if ((_linkOpts & HLINK_TARGET_FRAME) != 0) {
+ out.writeInt(_address.length()*2);
+ StringUtil.putUnicodeLE(_address, out);
+ } else {
+ out.writeInt(_address.length()*2 + TAIL_SIZE);
+ StringUtil.putUnicodeLE(_address, out);
+ writeTail(_uninterpretedTail, out);
}
- } else if (Arrays.equals(FILE_MONIKER, moniker)){
- LittleEndian.putShort(data, pos, file_opts); pos += 2;
- LittleEndian.putInt(data, pos, address.length()); pos += 4;
- byte[] bytes = address.getBytes();
- System.arraycopy(bytes, 0, data, pos, bytes.length); pos += bytes.length;
- if(tail.length > 0){
- System.arraycopy(tail, 0, data, pos, tail.length); pos += tail.length;
+ } else if (FILE_MONIKER.equals(_moniker)){
+ out.writeShort(_fileOpts);
+ out.writeInt(_shortFilename.length());
+ StringUtil.putCompressedUnicode(_shortFilename, out);
+ writeTail(_uninterpretedTail, out);
+ if (_address == null) {
+ out.writeInt(0);
+ } else {
+ int addrLen = _address.length() * 2;
+ out.writeInt(addrLen + 6);
+ out.writeInt(addrLen);
+ out.writeShort(0x0003); // TODO const
+ StringUtil.putUnicodeLE(_address, out);
}
}
- } else if((link_opts & HLINK_PLACE) != 0){
- LittleEndian.putInt(data, pos, address.length()); pos += 4;
- StringUtil.putUnicodeLE(address, data, pos); pos += address.length()*2;
}
- return getRecordSize();
+ if((_linkOpts & HLINK_PLACE) != 0){
+ out.writeInt(_textMark.length());
+ StringUtil.putUnicodeLE(_textMark, out);
+ }
}
protected int getDataSize() {
int size = 0;
size += 2 + 2 + 2 + 2; //rwFirst, rwLast, colFirst, colLast
- size += guid.length;
+ size += GUID.ENCODED_SIZE;
size += 4; //label_opts
size += 4; //link_opts
- if ((link_opts & HLINK_LABEL) != 0){
+ if ((_linkOpts & HLINK_LABEL) != 0){
size += 4; //link length
- size += label.length()*2;
+ size += _label.length()*2;
}
- if ((link_opts & HLINK_URL) != 0){
- size += moniker.length; //moniker length
- if(Arrays.equals(URL_MONIKER, moniker)){
+ if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
+ size += 4; // int nChars
+ size += _targetFrame.length()*2;
+ }
+ if ((_linkOpts & HLINK_UNC_PATH) != 0) {
+ size += 4; // int nChars
+ size += _address.length()*2;
+ } else if ((_linkOpts & HLINK_URL) != 0){
+ size += GUID.ENCODED_SIZE;
+ if(URL_MONIKER.equals(_moniker)){
size += 4; //address length
- size += address.length()*2;
- size += tail.length;
- } else if (Arrays.equals(FILE_MONIKER, moniker)){
+ size += _address.length()*2;
+ if ((_linkOpts & HLINK_TARGET_FRAME) == 0) {
+ size += TAIL_SIZE;
+ }
+ } else if (FILE_MONIKER.equals(_moniker)){
size += 2; //file_opts
size += 4; //address length
- size += address.length();
- size += tail.length;
+ size += _shortFilename.length();
+ size += TAIL_SIZE;
+ size += 4;
+ if (_address != null) {
+ size += 6;
+ size += _address.length() * 2;
+ }
+
}
- } else if((link_opts & HLINK_PLACE) != 0){
+ }
+ if((_linkOpts & HLINK_PLACE) != 0){
size += 4; //address length
- size += address.length()*2;
+ size += _textMark.length()*2;
}
return size;
}
- public String toString()
- {
+
+ private static byte[] readTail(byte[] expectedTail, LittleEndianInput in) {
+ byte[] result = new byte[TAIL_SIZE];
+ in.readFully(result);
+ if (false) { // Quite a few examples in the unit tests which don't have the exact expected tail
+ for (int i = 0; i < expectedTail.length; i++) {
+ if (expectedTail[i] != result[i]) {
+ System.err.println("Mismatch in tail byte [" + i + "]"
+ + "expected " + (expectedTail[i] & 0xFF) + " but got " + (result[i] & 0xFF));
+ }
+ }
+ }
+ return result;
+ }
+ private static void writeTail(byte[] tail, LittleEndianOutput out) {
+ out.write(tail);
+ }
+
+ public short getSid() {
+ return HyperlinkRecord.sid;
+ }
+
+
+ public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("[HYPERLINK RECORD]\n");
- buffer.append(" .rwFirst = ").append(Integer.toHexString(getFirstRow())).append("\n");
- buffer.append(" .rwLast = ").append(Integer.toHexString(getLastRow())).append("\n");
- buffer.append(" .colFirst = ").append(Integer.toHexString(getFirstColumn())).append("\n");
- buffer.append(" .colLast = ").append(Integer.toHexString(getLastColumn())).append("\n");
- buffer.append(" .guid = ").append(HexDump.toHex(guid)).append("\n");
- buffer.append(" .label_opts = ").append(label_opts).append("\n");
- buffer.append(" .label = ").append(getLabel()).append("\n");
- if((link_opts & HLINK_URL) != 0){
- buffer.append(" .moniker = ").append(HexDump.toHex(moniker)).append("\n");
+ buffer.append(" .range = ").append(_range.formatAsString()).append("\n");
+ buffer.append(" .guid = ").append(_guid.formatAsString()).append("\n");
+ buffer.append(" .linkOpts= ").append(HexDump.intToHex(_linkOpts)).append("\n");
+ buffer.append(" .label = ").append(getLabel()).append("\n");
+ if ((_linkOpts & HLINK_TARGET_FRAME) != 0) {
+ buffer.append(" .targetFrame= ").append(getTargetFrame()).append("\n");
}
- buffer.append(" .address = ").append(getAddress()).append("\n");
+ if((_linkOpts & HLINK_URL) != 0) {
+ buffer.append(" .moniker = ").append(_moniker.formatAsString()).append("\n");
+ }
+ if ((_linkOpts & HLINK_PLACE) != 0) {
+ buffer.append(" .targetFrame= ").append(getTextMark()).append("\n");
+ }
+ buffer.append(" .address = ").append(getAddress()).append("\n");
buffer.append("[/HYPERLINK RECORD]\n");
return buffer.toString();
}
@@ -458,71 +642,57 @@ public final class HyperlinkRecord extends Record {
/**
* Initialize a new url link
*/
- public void newUrlLink(){
- rwFirst = 0;
- rwLast = 0;
- colFirst = 0;
- colLast = 0;
- guid = STD_MONIKER;
- label_opts = 0x2;
- link_opts = HLINK_URL | HLINK_ABS | HLINK_LABEL;
- label = "" + '\u0000';
- moniker = URL_MONIKER;
- address = "" + '\u0000';
- tail = URL_TAIL;
+ public void newUrlLink() {
+ _range = new CellRangeAddress(0, 0, 0, 0);
+ _guid = STD_MONIKER;
+ _linkOpts = HLINK_URL | HLINK_ABS | HLINK_LABEL;
+ setLabel("");
+ _moniker = URL_MONIKER;
+ setAddress("");
+ _uninterpretedTail = URL_TAIL;
}
/**
* Initialize a new file link
*/
- public void newFileLink(){
- rwFirst = 0;
- rwLast = 0;
- colFirst = 0;
- colLast = 0;
- guid = STD_MONIKER;
- label_opts = 0x2;
- link_opts = HLINK_URL | HLINK_LABEL;
- file_opts = 0;
- label = "" + '\u0000';
- moniker = FILE_MONIKER;
- address = "" + '\0';
- tail = FILE_TAIL;
+ public void newFileLink() {
+ _range = new CellRangeAddress(0, 0, 0, 0);
+ _guid = STD_MONIKER;
+ _linkOpts = HLINK_URL | HLINK_LABEL;
+ _fileOpts = 0;
+ setLabel("");
+ _moniker = FILE_MONIKER;
+ setAddress(null);
+ setShortFilename("");
+ _uninterpretedTail = FILE_TAIL;
}
/**
* Initialize a new document link
*/
- public void newDocumentLink(){
- rwFirst = 0;
- rwLast = 0;
- colFirst = 0;
- colLast = 0;
- guid = STD_MONIKER;
- label_opts = 0x2;
- link_opts = HLINK_LABEL | HLINK_PLACE;
- label = "" + '\u0000';
- moniker = FILE_MONIKER;
- address = "" + '\0';
- tail = new byte[]{};
+ public void newDocumentLink() {
+ _range = new CellRangeAddress(0, 0, 0, 0);
+ _guid = STD_MONIKER;
+ _linkOpts = HLINK_LABEL | HLINK_PLACE;
+ setLabel("");
+ _moniker = FILE_MONIKER;
+ setAddress("");
+ setTextMark("");
}
public Object clone() {
HyperlinkRecord rec = new HyperlinkRecord();
- rec.rwFirst = rwFirst;
- rec.rwLast = rwLast;
- rec.colFirst = colFirst;
- rec.colLast = colLast;
- rec.guid = guid;
- rec.label_opts = label_opts;
- rec.link_opts = link_opts;
- rec.file_opts = file_opts;
- rec.label = label;
- rec.address = address;
- rec.moniker = moniker;
- rec.tail = tail;
+ rec._range = _range.copy();
+ rec._guid = _guid;
+ rec._linkOpts = _linkOpts;
+ rec._fileOpts = _fileOpts;
+ rec._label = _label;
+ rec._address = _address;
+ rec._moniker = _moniker;
+ rec._shortFilename = _shortFilename;
+ rec._targetFrame = _targetFrame;
+ rec._textMark = _textMark;
+ rec._uninterpretedTail = _uninterpretedTail;
return rec;
}
-
-
}
diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java b/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java
index 3bfbf880b..590e706b8 100755
--- a/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java
+++ b/src/java/org/apache/poi/hssf/usermodel/HSSFHyperlink.java
@@ -169,6 +169,18 @@ public class HSSFHyperlink implements Hyperlink {
public String getAddress(){
return record.getAddress();
}
+ public String getTextMark(){
+ return record.getTextMark();
+ }
+ public void setTextMark(String textMark) {
+ record.setTextMark(textMark);
+ }
+ public String getShortFilename(){
+ return record.getShortFilename();
+ }
+ public void setShortFilename(String shortFilename) {
+ record.setShortFilename(shortFilename);
+ }
/**
* Hypelink address. Depending on the hyperlink type it can be URL, e-mail, patrh to a file, etc.
diff --git a/src/testcases/org/apache/poi/hssf/record/TestHyperlinkRecord.java b/src/testcases/org/apache/poi/hssf/record/TestHyperlinkRecord.java
index 4b73522a3..60cccf9a4 100644
--- a/src/testcases/org/apache/poi/hssf/record/TestHyperlinkRecord.java
+++ b/src/testcases/org/apache/poi/hssf/record/TestHyperlinkRecord.java
@@ -16,11 +16,17 @@
==================================================================== */
package org.apache.poi.hssf.record;
-import java.io.ByteArrayInputStream;
import java.util.Arrays;
+import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
+import org.apache.poi.hssf.record.HyperlinkRecord.GUID;
+import org.apache.poi.util.HexDump;
+import org.apache.poi.util.HexRead;
+import org.apache.poi.util.LittleEndianByteArrayInputStream;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+
/**
* Test HyperlinkRecord
*
@@ -98,7 +104,7 @@ public final class TestHyperlinkRecord extends TestCase {
(byte)0xFF, (byte)0xFF, (byte)0xAD, (byte)0xDE, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
-
+
0x00, 0x00, 0x00, 0x00, // length of address link field
};
@@ -143,7 +149,7 @@ public final class TestHyperlinkRecord extends TestCase {
};
//link to a place in worksheet: Sheet1!A1
- byte[] data4 = {0x03, 0x00,
+ private static final byte[] data4 = {0x03, 0x00,
0x03, 0x00,
0x00, 0x00,
0x00, 0x00,
@@ -166,8 +172,79 @@ public final class TestHyperlinkRecord extends TestCase {
0x53, 0x00, 0x68, 0x00, 0x65, 0x00, 0x65, 0x00, 0x74, 0x00, 0x31, 0x00, 0x21,
0x00, 0x41, 0x00, 0x31, 0x00, 0x00, 0x00};
- private void confirmGUID(byte[] expectedGuid, byte[] actualGuid) {
- assertTrue(Arrays.equals(expectedGuid, actualGuid));
+ private static final byte[] dataLinkToWorkbook = HexRead.readFromString("01 00 01 00 01 00 01 00 " +
+ "D0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B " +
+ "02 00 00 00 " +
+ "1D 00 00 00 " + // options: LABEL | PLACE | FILE_OR_URL
+ // label: "My Label"
+ "09 00 00 00 " +
+ "4D 00 79 00 20 00 4C 00 61 00 62 00 65 00 6C 00 00 00 " +
+ "03 03 00 00 00 00 00 00 C0 00 00 00 00 00 00 46 " + // file GUID
+ "00 00 " + // file options
+ // shortFileName: "YEARFR~1.XLS"
+ "0D 00 00 00 " +
+ "59 45 41 52 46 52 7E 31 2E 58 4C 53 00 " +
+ // FILE_TAIL - unknown byte sequence
+ "FF FF AD DE 00 00 00 00 " +
+ "00 00 00 00 00 00 00 00 " +
+ "00 00 00 00 00 00 00 00 " +
+ // field len, char data len
+ "2E 00 00 00 " +
+ "28 00 00 00 " +
+ "03 00 " + // unknown ushort
+ // _address: "yearfracExamples.xls"
+ "79 00 65 00 61 00 72 00 66 00 72 00 61 00 63 00 " +
+ "45 00 78 00 61 00 6D 00 70 00 6C 00 65 00 73 00 " +
+ "2E 00 78 00 6C 00 73 00 " +
+ // textMark: "Sheet1!B6"
+ "0A 00 00 00 " +
+ "53 00 68 00 65 00 65 00 74 00 31 00 21 00 42 00 " +
+ "36 00 00 00");
+
+ private static final byte[] dataTargetFrame = HexRead.readFromString("0E 00 0E 00 00 00 00 00 " +
+ "D0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B " +
+ "02 00 00 00 " +
+ "83 00 00 00 " + // options: TARGET_FRAME | ABS | FILE_OR_URL
+ // targetFrame: "_blank"
+ "07 00 00 00 " +
+ "5F 00 62 00 6C 00 61 00 6E 00 6B 00 00 00 " +
+ // url GUID
+ "E0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B " +
+ // address: "http://www.regnow.com/softsell/nph-softsell.cgi?currency=USD&item=7924-37"
+ "94 00 00 00 " +
+ "68 00 74 00 74 00 70 00 3A 00 2F 00 2F 00 77 00 " +
+ "77 00 77 00 2E 00 72 00 65 00 67 00 6E 00 6F 00 " +
+ "77 00 2E 00 63 00 6F 00 6D 00 2F 00 73 00 6F 00 " +
+ "66 00 74 00 73 00 65 00 6C 00 6C 00 2F 00 6E 00 " +
+ "70 00 68 00 2D 00 73 00 6F 00 66 00 74 00 73 00 " +
+ "65 00 6C 00 6C 00 2E 00 63 00 67 00 69 00 3F 00 " +
+ "63 00 75 00 72 00 72 00 65 00 6E 00 63 00 79 00 " +
+ "3D 00 55 00 53 00 44 00 26 00 69 00 74 00 65 00 " +
+ "6D 00 3D 00 37 00 39 00 32 00 34 00 2D 00 33 00 " +
+ "37 00 00 00");
+
+
+ private static final byte[] dataUNC = HexRead.readFromString("01 00 01 00 01 00 01 00 " +
+ "D0 C9 EA 79 F9 BA CE 11 8C 82 00 AA 00 4B A9 0B " +
+ "02 00 00 00 " +
+ "1F 01 00 00 " + // options: UNC_PATH | LABEL | TEXT_MARK | ABS | FILE_OR_URL
+ "09 00 00 00 " + // label: "My Label"
+ "4D 00 79 00 20 00 6C 00 61 00 62 00 65 00 6C 00 00 00 " +
+ // note - no moniker GUID
+ "27 00 00 00 " + // "\\\\MyServer\\my-share\\myDir\\PRODNAME.xls"
+ "5C 00 5C 00 4D 00 79 00 53 00 65 00 72 00 76 00 " +
+ "65 00 72 00 5C 00 6D 00 79 00 2D 00 73 00 68 00 " +
+ "61 00 72 00 65 00 5C 00 6D 00 79 00 44 00 69 00 " +
+ "72 00 5C 00 50 00 52 00 4F 00 44 00 4E 00 41 00 " +
+ "4D 00 45 00 2E 00 78 00 6C 00 73 00 00 00 " +
+
+ "0C 00 00 00 " + // textMark: PRODNAME!C2
+ "50 00 52 00 4F 00 44 00 4E 00 41 00 4D 00 45 00 21 00 " +
+ "43 00 32 00 00 00");
+
+
+ private void confirmGUID(GUID expectedGuid, GUID actualGuid) {
+ assertEquals(expectedGuid, actualGuid);
}
public void testReadURLLink(){
RecordInputStream is = TestcaseRecordInputStream.create(HyperlinkRecord.sid, data1);
@@ -203,7 +280,8 @@ public final class TestHyperlinkRecord extends TestCase {
assertEquals(opts, link.getLinkOptions());
assertEquals("file", link.getLabel());
- assertEquals("link1.xls", link.getAddress());
+ assertEquals("link1.xls", link.getShortFilename());
+ assertEquals(null, link.getAddress());
}
public void testReadEmailLink(){
@@ -238,7 +316,8 @@ public final class TestHyperlinkRecord extends TestCase {
assertEquals(opts, link.getLinkOptions());
assertEquals("place", link.getLabel());
- assertEquals("Sheet1!A1", link.getAddress());
+ assertEquals("Sheet1!A1", link.getTextMark());
+ assertEquals(null, link.getAddress());
}
private void serialize(byte[] data){
@@ -280,7 +359,7 @@ public final class TestHyperlinkRecord extends TestCase {
link.setFirstRow((short)0);
link.setLastRow((short)0);
link.setLabel("file");
- link.setAddress("link1.xls");
+ link.setShortFilename("link1.xls");
byte[] tmp = link.serialize();
byte[] ser = new byte[tmp.length-4];
@@ -295,7 +374,7 @@ public final class TestHyperlinkRecord extends TestCase {
link.setFirstRow((short)3);
link.setLastRow((short)3);
link.setLabel("place");
- link.setAddress("Sheet1!A1");
+ link.setTextMark("Sheet1!A1");
byte[] tmp = link.serialize();
byte[] ser = new byte[tmp.length-4];
@@ -329,4 +408,70 @@ public final class TestHyperlinkRecord extends TestCase {
}
}
+
+ public void testReserializeTargetFrame() {
+ RecordInputStream in = TestcaseRecordInputStream.create(HyperlinkRecord.sid, dataTargetFrame);
+ HyperlinkRecord hr = new HyperlinkRecord(in);
+ byte[] ser = hr.serialize();
+ TestcaseRecordInputStream.confirmRecordEncoding(HyperlinkRecord.sid, dataTargetFrame, ser);
+ }
+
+
+ public void testReserializeLinkToWorkbook() {
+
+ RecordInputStream in = TestcaseRecordInputStream.create(HyperlinkRecord.sid, dataLinkToWorkbook);
+ HyperlinkRecord hr = new HyperlinkRecord(in);
+ byte[] ser = hr.serialize();
+ TestcaseRecordInputStream.confirmRecordEncoding(HyperlinkRecord.sid, dataLinkToWorkbook, ser);
+ if ("YEARFR~1.XLS".equals(hr.getAddress())) {
+ throw new AssertionFailedError("Identified bug in reading workbook link");
+ }
+ assertEquals("yearfracExamples.xls", hr.getAddress());
+ }
+
+ public void testReserializeUNC() {
+
+ RecordInputStream in = TestcaseRecordInputStream.create(HyperlinkRecord.sid, dataUNC);
+ HyperlinkRecord hr = new HyperlinkRecord(in);
+ byte[] ser = hr.serialize();
+ TestcaseRecordInputStream.confirmRecordEncoding(HyperlinkRecord.sid, dataUNC, ser);
+ }
+
+ public void testGUID() {
+ GUID g;
+ g = GUID.parse("3F2504E0-4F89-11D3-9A0C-0305E82C3301");
+ confirmGUID(g, 0x3F2504E0, 0x4F89, 0x11D3, 0x9A0C0305E82C3301L);
+ assertEquals("3F2504E0-4F89-11D3-9A0C-0305E82C3301", g.formatAsString());
+
+ g = GUID.parse("13579BDF-0246-8ACE-0123-456789ABCDEF");
+ confirmGUID(g, 0x13579BDF, 0x0246, 0x8ACE, 0x0123456789ABCDEFL);
+ assertEquals("13579BDF-0246-8ACE-0123-456789ABCDEF", g.formatAsString());
+
+ byte[] buf = new byte[16];
+ g.serialize(new LittleEndianByteArrayOutputStream(buf, 0));
+ String expectedDump = "[DF, 9B, 57, 13, 46, 02, CE, 8A, 01, 23, 45, 67, 89, AB, CD, EF]";
+ assertEquals(expectedDump, HexDump.toHex(buf));
+
+ // STD Moniker
+ g = createFromStreamDump("[D0, C9, EA, 79, F9, BA, CE, 11, 8C, 82, 00, AA, 00, 4B, A9, 0B]");
+ assertEquals("79EAC9D0-BAF9-11CE-8C82-00AA004BA90B", g.formatAsString());
+ // URL Moniker
+ g = createFromStreamDump("[E0, C9, EA, 79, F9, BA, CE, 11, 8C, 82, 00, AA, 00, 4B, A9, 0B]");
+ assertEquals("79EAC9E0-BAF9-11CE-8C82-00AA004BA90B", g.formatAsString());
+ // File Moniker
+ g = createFromStreamDump("[03, 03, 00, 00, 00, 00, 00, 00, C0, 00, 00, 00, 00, 00, 00, 46]");
+ assertEquals("00000303-0000-0000-C000-000000000046", g.formatAsString());
+ }
+
+ private static GUID createFromStreamDump(String s) {
+ return new GUID(new LittleEndianByteArrayInputStream(HexRead.readFromString(s)));
+ }
+
+ private void confirmGUID(GUID g, int d1, int d2, int d3, long d4) {
+ assertEquals(new String(HexDump.intToHex(d1)), new String(HexDump.intToHex(g.getD1())));
+ assertEquals(new String(HexDump.shortToHex(d2)), new String(HexDump.shortToHex(g.getD2())));
+ assertEquals(new String(HexDump.shortToHex(d3)), new String(HexDump.shortToHex(g.getD3())));
+ assertEquals(new String(HexDump.longToHex(d4)), new String(HexDump.longToHex(g.getD4())));
+ }
+
}
diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHyperlink.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHyperlink.java
index 071e265bd..e741166b0 100755
--- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHyperlink.java
+++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHyperlink.java
@@ -70,7 +70,7 @@ public final class TestHSSFHyperlink extends TestCase {
assertNotNull(link);
assertEquals("Link To First Sheet", link.getLabel());
assertEquals("Link To First Sheet", cell.getRichStringCellValue().getString());
- assertEquals("WebLinks!A1", link.getAddress());
+ assertEquals("WebLinks!A1", link.getTextMark());
}
public void testModify() throws Exception {
@@ -136,7 +136,7 @@ public final class TestHSSFHyperlink extends TestCase {
cell = sheet.createRow(3).createCell(0);
cell.setCellValue("Worksheet Link");
link = new HSSFHyperlink(HSSFHyperlink.LINK_DOCUMENT);
- link.setAddress("'Target Sheet'!A1");
+ link.setTextMark("'Target Sheet'!A1");
cell.setHyperlink(link);
//serialize and read again
@@ -163,7 +163,7 @@ public final class TestHSSFHyperlink extends TestCase {
cell = sheet.getRow(3).getCell(0);
link = cell.getHyperlink();
assertNotNull(link);
- assertEquals("'Target Sheet'!A1", link.getAddress());
+ assertEquals("'Target Sheet'!A1", link.getTextMark());
}
public void testCloneSheet() {