123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625 |
- /* ====================================================================
- 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 static org.apache.logging.log4j.util.Unbox.box;
- import static org.apache.poi.hpsf.ClassIDPredefined.FILE_MONIKER;
- import static org.apache.poi.hpsf.ClassIDPredefined.STD_MONIKER;
- import static org.apache.poi.hpsf.ClassIDPredefined.URL_MONIKER;
- import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
- import static org.apache.poi.util.HexDump.toHex;
-
- import java.util.Map;
- import java.util.function.Supplier;
-
- import org.apache.logging.log4j.LogManager;
- import org.apache.logging.log4j.Logger;
- import org.apache.poi.hpsf.ClassID;
- import org.apache.poi.hpsf.ClassIDPredefined;
- import org.apache.poi.ss.util.CellRangeAddress;
- import org.apache.poi.util.GenericRecordUtil;
- import org.apache.poi.util.HexRead;
- import org.apache.poi.util.IOUtils;
- import org.apache.poi.util.LittleEndianInput;
- import org.apache.poi.util.LittleEndianOutput;
- import org.apache.poi.util.RecordFormatException;
- import org.apache.poi.util.StringUtil;
-
- /**
- * The <code>HyperlinkRecord</code> (0x01B8) wraps an HLINK-record
- * from the Excel-97 format.
- * Supports only external links for now (eg http://)
- */
- public final class HyperlinkRecord extends StandardRecord {
- public static final short sid = 0x01B8;
- private static final Logger LOG = LogManager.getLogger(HyperlinkRecord.class);
- //arbitrarily selected; may need to increase
- private static final int MAX_RECORD_LENGTH = 100_000;
-
-
- /*
- * Link flags
- */
- 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
-
- /** expected Tail of a URL link */
- private static final 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 static final 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");
-
- private static final int TAIL_SIZE = FILE_TAIL.length;
-
- /** cell range of this hyperlink */
- private CellRangeAddress _range;
-
- /** 16-byte GUID */
- private ClassID _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 ClassID _moniker;
- /** in 8:3 DOS format No Unicode string header,
- * always 8-bit characters, zero-terminated */
- private String _shortFilename;
- /** Link */
- private String _address;
- /**
- * Text describing a place in document. In Excel UI, this is appended to the
- * address, (after a '#' delimiter).<br>
- * This field is optional. If present, the {@link #HLINK_PLACE} must be set.
- */
- private String _textMark;
-
- private byte[] _uninterpretedTail;
-
- /**
- * Create a new hyperlink
- */
- public HyperlinkRecord() {}
-
-
- public HyperlinkRecord(HyperlinkRecord other) {
- super(other);
- _range = (other._range == null) ? null : other._range.copy();
- _guid = (other._guid == null) ? null : other._guid.copy();
- _fileOpts = other._fileOpts;
- _linkOpts = other._linkOpts;
- _label = other._label;
- _targetFrame = other._targetFrame;
- _moniker = (other._moniker == null) ? null : other._moniker.copy();
- _shortFilename = other._shortFilename;
- _address = other._address;
- _textMark = other._textMark;
- _uninterpretedTail = (other._uninterpretedTail == null) ? null : other._uninterpretedTail.clone();
- }
-
-
- /**
- * @return the 0-based column of the first cell that contains this hyperlink
- */
- public int getFirstColumn() {
- return _range.getFirstColumn();
- }
-
- /**
- * Set the first column (zero-based) of the range that contains this hyperlink
- *
- * @param firstCol the first column (zero-based)
- */
- public void setFirstColumn(int firstCol) {
- _range.setFirstColumn(firstCol);
- }
-
- /**
- * @return the 0-based column of the last cell that contains this hyperlink
- */
- public int getLastColumn() {
- return _range.getLastColumn();
- }
-
- /**
- * Set the last column (zero-based) of the range that contains this hyperlink
- *
- * @param lastCol the last column (zero-based)
- */
- public void setLastColumn(int lastCol) {
- _range.setLastColumn(lastCol);
- }
-
- /**
- * @return the 0-based row of the first cell that contains this hyperlink
- */
- public int getFirstRow() {
- return _range.getFirstRow();
- }
-
- /**
- * Set the first row (zero-based) of the range that contains this hyperlink
- *
- * @param firstRow the first row (zero-based)
- */
- public void setFirstRow(int firstRow) {
- _range.setFirstRow(firstRow);
- }
-
- /**
- * @return the 0-based row of the last cell that contains this hyperlink
- */
- public int getLastRow() {
- return _range.getLastRow();
- }
-
- /**
- * Set the last row (zero-based) of the range that contains this hyperlink
- *
- * @param lastRow the last row (zero-based)
- */
- public void setLastRow(int lastRow) {
- _range.setLastRow(lastRow);
- }
-
- /**
- * @return 16-byte guid identifier Seems to always equal {@link ClassIDPredefined#STD_MONIKER}
- */
- ClassID getGuid() {
- return _guid;
- }
-
- /**
- * @return 16-byte moniker
- */
- ClassID getMoniker()
- {
- 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() {
- return cleanString(_label);
- }
-
- /**
- * Sets text label for this hyperlink
- *
- * @param label text label for this hyperlink
- */
- public void setLabel(String label) {
- _label = appendNullTerm(label);
- }
- public String getTargetFrame() {
- return cleanString(_targetFrame);
- }
-
- /**
- * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, path to a file, etc.
- *
- * @return the address of this hyperlink
- */
- public String getAddress() {
- if ((_linkOpts & HLINK_URL) != 0 && FILE_MONIKER.equals(_moniker)) {
- return cleanString(_address != null ? _address : _shortFilename);
- } else if((_linkOpts & HLINK_PLACE) != 0) {
- return cleanString(_textMark);
- } else {
- return cleanString(_address);
- }
- }
-
- /**
- * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, path to a file, etc.
- *
- * @param address the address of this hyperlink
- */
- public void setAddress(String address) {
- if ((_linkOpts & HLINK_URL) != 0 && FILE_MONIKER.equals(_moniker)) {
- _shortFilename = appendNullTerm(address);
- } else if((_linkOpts & HLINK_PLACE) != 0) {
- _textMark = appendNullTerm(address);
- } else {
- _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
- *
- * @return Link options
- */
- int getLinkOptions(){
- return _linkOpts;
- }
-
- /**
- * @return Label options
- */
- public int getLabelOptions(){
- return 2; // always 2
- }
-
- /**
- * @return Options for a file link
- */
- public int getFileOptions(){
- return _fileOpts;
- }
-
-
- public HyperlinkRecord(RecordInputStream in) {
- _range = new CellRangeAddress(in);
-
- _guid = new ClassID(in);
-
- /*
- * streamVersion (4 bytes): An unsigned integer that specifies the version number
- * of the serialization implementation used to save this structure. This value MUST equal 2.
- */
- int streamVersion = in.readInt();
- if (streamVersion != 0x00000002) {
- throw new RecordFormatException("Stream Version must be 0x2 but found " + streamVersion);
- }
- _linkOpts = in.readInt();
-
- if ((_linkOpts & HLINK_LABEL) != 0){
- int label_len = in.readInt();
- _label = in.readUnicodeLEString(label_len);
- }
-
- if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
- int len = in.readInt();
- _targetFrame = in.readUnicodeLEString(len);
- }
-
- if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) {
- _moniker = null;
- int nChars = in.readInt();
- _address = in.readUnicodeLEString(nChars);
- }
-
- if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) {
- _moniker = new ClassID(in);
-
- if(URL_MONIKER.equals(_moniker)){
- int length = in.readInt();
- /*
- * The value of <code>length<code> be either the byte size of the url field
- * (including the terminating NULL character) or the byte size of the url field plus 24.
- * If the value of this field is set to the byte size of the url field,
- * then the tail bytes fields are not present.
- */
- int remaining = in.remaining();
- if (length == remaining) {
- int nChars = length/2;
- _address = in.readUnicodeLEString(nChars);
- } else {
- int nChars = (length - TAIL_SIZE)/2;
- _address = in.readUnicodeLEString(nChars);
- /*
- * TODO: make sense of the remaining bytes
- * According to the spec they consist of:
- * 1. 16-byte GUID: This field MUST equal
- * {0xF4815879, 0x1D3B, 0x487F, 0xAF, 0x2C, 0x82, 0x5D, 0xC4, 0x85, 0x27, 0x63}
- * 2. Serial version, this field MUST equal 0 if present.
- * 3. URI Flags
- */
- _uninterpretedTail = readTail(URL_TAIL, in);
- }
- } else if (FILE_MONIKER.equals(_moniker)) {
- _fileOpts = in.readShort();
-
- int len = in.readInt();
- _shortFilename = StringUtil.readCompressedUnicode(in, len);
- _uninterpretedTail = readTail(FILE_TAIL, in);
- int size = in.readInt();
- if (size > 0) {
- int charDataSize = in.readInt();
-
- //From the spec: An optional unsigned integer that MUST be 3 if present
- // but some files has 4
- /*int usKeyValue = */ in.readUShort();
-
- _address = StringUtil.readUnicodeLE(in, charDataSize/2);
- } else {
- _address = null;
- }
- } else if (STD_MONIKER.equals(_moniker)) {
- _fileOpts = in.readShort();
-
- int len = in.readInt();
-
- byte[] path_bytes = IOUtils.safelyAllocate(len, MAX_RECORD_LENGTH);
- in.readFully(path_bytes);
-
- _address = new String(path_bytes, StringUtil.UTF8);
- }
- }
-
- if((_linkOpts & HLINK_PLACE) != 0) {
-
- int len = in.readInt();
- _textMark = in.readUnicodeLEString(len);
- }
-
- if (in.remaining() > 0) {
- LOG.atWarn().log("Hyperlink data remains: {} : {}", box(in.remaining()), toHex(in.readRemainder()));
- }
- }
-
- @Override
- public void serialize(LittleEndianOutput out) {
- _range.serialize(out);
-
- _guid.write(out);
- out.writeInt(0x00000002); // TODO const
- out.writeInt(_linkOpts);
-
- if ((_linkOpts & HLINK_LABEL) != 0){
- out.writeInt(_label.length());
- StringUtil.putUnicodeLE(_label, out);
- }
- if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
- out.writeInt(_targetFrame.length());
- StringUtil.putUnicodeLE(_targetFrame, out);
- }
-
- if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) {
- out.writeInt(_address.length());
- StringUtil.putUnicodeLE(_address, out);
- }
-
- if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) {
- _moniker.write(out);
- if(URL_MONIKER.equals(_moniker)){
- if (_uninterpretedTail == null) {
- 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 (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);
- }
- }
- }
- if((_linkOpts & HLINK_PLACE) != 0){
- out.writeInt(_textMark.length());
- StringUtil.putUnicodeLE(_textMark, out);
- }
- }
-
- @Override
- protected int getDataSize() {
- int size = 0;
- size += 2 + 2 + 2 + 2; //rwFirst, rwLast, colFirst, colLast
- size += ClassID.LENGTH;
- size += 4; //label_opts
- size += 4; //link_opts
- if ((_linkOpts & HLINK_LABEL) != 0){
- size += 4; //link length
- size += _label.length()*2;
- }
- if ((_linkOpts & HLINK_TARGET_FRAME) != 0){
- size += 4; // int nChars
- size += _targetFrame.length()*2;
- }
- if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) {
- size += 4; // int nChars
- size += _address.length()*2;
- }
- if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) {
- size += ClassID.LENGTH;
- if(URL_MONIKER.equals(_moniker)){
- size += 4; //address length
- size += _address.length()*2;
- if (_uninterpretedTail != null) {
- size += TAIL_SIZE;
- }
- } else if (FILE_MONIKER.equals(_moniker)){
- size += 2; //file_opts
- size += 4; //address length
- size += _shortFilename.length();
- size += TAIL_SIZE;
- size += 4;
- if (_address != null) {
- size += 6;
- size += _address.length() * 2;
- }
-
- }
- }
- if((_linkOpts & HLINK_PLACE) != 0){
- size += 4; //address length
- size += _textMark.length()*2;
- }
- return size;
- }
-
-
- private static byte[] readTail(byte[] expectedTail, LittleEndianInput in) {
- byte[] result = new byte[TAIL_SIZE];
- in.readFully(result);
- return result;
- }
-
- private static void writeTail(byte[] tail, LittleEndianOutput out) {
- out.write(tail);
- }
-
- @Override
- public short getSid() {
- return HyperlinkRecord.sid;
- }
-
-
- /**
- * Based on the link options, is this a url?
- *
- * @return true, if this is a url link
- */
- @SuppressWarnings("unused")
- public boolean isUrlLink() {
- return (_linkOpts & HLINK_URL) > 0
- && (_linkOpts & HLINK_ABS) > 0;
- }
- /**
- * Based on the link options, is this a file?
- *
- * @return true, if this is a file link
- */
- public boolean isFileLink() {
- return (_linkOpts & HLINK_URL) > 0
- && (_linkOpts & HLINK_ABS) == 0;
- }
- /**
- * Based on the link options, is this a document?
- *
- * @return true, if this is a docment link
- */
- public boolean isDocumentLink() {
- return (_linkOpts & HLINK_PLACE) > 0;
- }
-
- /**
- * Initialize a new url link
- */
- public void newUrlLink() {
- _range = new CellRangeAddress(0, 0, 0, 0);
- _guid = STD_MONIKER.getClassID();
- _linkOpts = HLINK_URL | HLINK_ABS | HLINK_LABEL;
- setLabel("");
- _moniker = URL_MONIKER.getClassID();
- setAddress("");
- _uninterpretedTail = URL_TAIL;
- }
-
- /**
- * Initialize a new file link
- */
- public void newFileLink() {
- _range = new CellRangeAddress(0, 0, 0, 0);
- _guid = STD_MONIKER.getClassID();
- _linkOpts = HLINK_URL | HLINK_LABEL;
- _fileOpts = 0;
- setLabel("");
- _moniker = FILE_MONIKER.getClassID();
- setAddress(null);
- setShortFilename("");
- _uninterpretedTail = FILE_TAIL;
- }
-
- /**
- * Initialize a new document link
- */
- public void newDocumentLink() {
- _range = new CellRangeAddress(0, 0, 0, 0);
- _guid = STD_MONIKER.getClassID();
- _linkOpts = HLINK_LABEL | HLINK_PLACE;
- setLabel("");
- _moniker = FILE_MONIKER.getClassID();
- setAddress("");
- setTextMark("");
- }
-
- @Override
- public HyperlinkRecord copy() {
- return new HyperlinkRecord(this);
- }
-
- @Override
- public HSSFRecordTypes getGenericRecordType() {
- return HSSFRecordTypes.HYPERLINK;
- }
-
- @Override
- public Map<String, Supplier<?>> getGenericProperties() {
- return GenericRecordUtil.getGenericProperties(
- "range", () -> _range,
- "guid", this::getGuid,
- "linkOpts", () -> getBitsAsString(this::getLinkOptions,
- new int[]{HLINK_URL,HLINK_ABS,HLINK_PLACE,HLINK_LABEL,HLINK_TARGET_FRAME,HLINK_UNC_PATH},
- new String[]{"URL","ABS","PLACE","LABEL","TARGET_FRAME","UNC_PATH"}),
- "label", this::getLabel,
- "targetFrame", this::getTargetFrame,
- "moniker", this::getMoniker,
- "textMark", this::getTextMark,
- "address", this::getAddress
- );
- }
- }
|