Example #1
0
/**
 * The base abstract record from which all escher records are defined. Subclasses will need to
 * define methods for serialization/deserialization and for determining the record size.
 *
 * @author Glen Stampoultzis
 */
public abstract class EscherRecord {
  private static BitField fInstance = BitFieldFactory.getInstance(0xfff0);
  private static BitField fVersion = BitFieldFactory.getInstance(0x000f);

  private short _options;
  private short _recordId;

  /** Create a new instance */
  public EscherRecord() {
    // fields uninitialised
  }

  /**
   * Delegates to fillFields(byte[], int, EscherRecordFactory)
   *
   * @see #fillFields(byte[], int, org.apache.poi.ddf.EscherRecordFactory)
   */
  protected int fillFields(byte[] data, EscherRecordFactory f) {
    return fillFields(data, 0, f);
  }

  /**
   * The contract of this method is to deserialize an escher record including it's children.
   *
   * @param data The byte array containing the serialized escher records.
   * @param offset The offset into the byte array.
   * @param recordFactory A factory for creating new escher records.
   * @return The number of bytes written.
   */
  public abstract int fillFields(byte[] data, int offset, EscherRecordFactory recordFactory);

  /**
   * Reads the 8 byte header information and populates the <code>options</code> and <code>recordId
   * </code> records.
   *
   * @param data the byte array to read from
   * @param offset the offset to start reading from
   * @return the number of bytes remaining in this record. This may include the children if this is
   *     a container.
   */
  protected int readHeader(byte[] data, int offset) {
    _options = LittleEndian.getShort(data, offset);
    _recordId = LittleEndian.getShort(data, offset + 2);
    int remainingBytes = LittleEndian.getInt(data, offset + 4);
    return remainingBytes;
  }

  /**
   * Read the options field from header and return instance part of it.
   *
   * @param data the byte array to read from
   * @param offset the offset to start reading from
   * @return value of instance part of options field
   */
  protected static short readInstance(byte data[], int offset) {
    final short options = LittleEndian.getShort(data, offset);
    return fInstance.getShortValue(options);
  }

  /**
   * Determine whether this is a container record by inspecting the option field.
   *
   * @return true is this is a container field.
   */
  public boolean isContainerRecord() {
    return getVersion() == (short) 0x000f;
  }

  /**
   * options</code> is an internal field. Use {@link #setInstance(short)} ()} and {@link
   * #setVersion(short)} ()} to set the actual fields.
   *
   * @return The options field for this record. All records have one.
   */
  @Internal
  public short getOptions() {
    return _options;
  }

  /**
   * Set the options this this record. Container records should have the last nibble set to 0xF.
   *
   * <p
   * Note that <code>options</code> is an internal field. Use {@link #getInstance()} and
   * {@link #getVersion()} to access actual fields.
   */
  @Internal
  public void setOptions(short options) {
    // call to handle correct/incorrect values
    setVersion(fVersion.getShortValue(options));
    setInstance(fInstance.getShortValue(options));
    _options = options;
  }

  /**
   * Serializes to a new byte array. This is done by delegating to serialize(int, byte[]);
   *
   * @return the serialized record.
   * @see #serialize(int, byte[])
   */
  public byte[] serialize() {
    byte[] retval = new byte[getRecordSize()];

    serialize(0, retval);
    return retval;
  }

  /**
   * Serializes to an existing byte array without serialization listener. This is done by delegating
   * to serialize(int, byte[], EscherSerializationListener).
   *
   * @param offset the offset within the data byte array.
   * @param data the data array to serialize to.
   * @return The number of bytes written.
   * @see #serialize(int, byte[], org.apache.poi.ddf.EscherSerializationListener)
   */
  public int serialize(int offset, byte[] data) {
    return serialize(offset, data, new NullEscherSerializationListener());
  }

  /**
   * Serializes the record to an existing byte array.
   *
   * @param offset the offset within the byte array
   * @param data the data array to serialize to
   * @param listener a listener for begin and end serialization events. This is useful because the
   *     serialization is hierarchical/recursive and sometimes you need to be able break into that.
   * @return the number of bytes written.
   */
  public abstract int serialize(int offset, byte[] data, EscherSerializationListener listener);

  /**
   * Subclasses should effeciently return the number of bytes required to serialize the record.
   *
   * @return number of bytes
   */
  public abstract int getRecordSize();

  /**
   * Return the current record id.
   *
   * @return The 16 bit record id.
   */
  public short getRecordId() {
    return _recordId;
  }

  /** Sets the record id for this record. */
  public void setRecordId(short recordId) {
    _recordId = recordId;
  }

  /**
   * @return Returns the children of this record. By default this will be an empty list.
   *     EscherCotainerRecord is the only record that may contain children.
   * @see EscherContainerRecord
   */
  public List<EscherRecord> getChildRecords() {
    return Collections.emptyList();
  }

  /**
   * Sets the child records for this record. By default this will throw an exception as only
   * EscherContainerRecords may have children.
   *
   * @param childRecords Not used in base implementation.
   */
  public void setChildRecords(List<EscherRecord> childRecords) {
    throw new UnsupportedOperationException("This record does not support child records.");
  }

  /** Escher records may need to be clonable in the future. */
  public Object clone() {
    throw new RuntimeException(
        "The class " + getClass().getName() + " needs to define a clone method");
  }

  /** Returns the indexed child record. */
  public EscherRecord getChild(int index) {
    return getChildRecords().get(index);
  }

  /**
   * The display methods allows escher variables to print the record names according to their
   * hierarchy.
   *
   * @param w The print writer to output to.
   * @param indent The current indent level.
   */
  public void display(PrintWriter w, int indent) {
    for (int i = 0; i < indent * 4; i++) w.print(' ');
    w.println(getRecordName());
  }

  /** Subclasses should return the short name for this escher record. */
  public abstract String getRecordName();

  /**
   * Returns the instance part of the option record.
   *
   * @return The instance part of the record
   */
  public short getInstance() {
    return fInstance.getShortValue(_options);
  }

  /**
   * Sets the instance part of record
   *
   * @param value instance part value
   */
  public void setInstance(short value) {
    _options = fInstance.setShortValue(_options, value);
  }

  /**
   * Returns the version part of the option record.
   *
   * @return The version part of the option record
   */
  public short getVersion() {
    return fVersion.getShortValue(_options);
  }

  /**
   * Sets the version part of record
   *
   * @param value version part value
   */
  public void setVersion(short value) {
    _options = fVersion.setShortValue(_options, value);
  }

  /**
   * @param tab - each children must be a right of his parent
   * @return xml representation of this record
   */
  public String toXml(String tab) {
    StringBuilder builder = new StringBuilder();
    builder
        .append(tab)
        .append("<")
        .append(getClass().getSimpleName())
        .append(">\n")
        .append(tab)
        .append("\t")
        .append("<RecordId>0x")
        .append(HexDump.toHex(_recordId))
        .append("</RecordId>\n")
        .append(tab)
        .append("\t")
        .append("<Options>")
        .append(_options)
        .append("</Options>\n")
        .append(tab)
        .append("</")
        .append(getClass().getSimpleName())
        .append(">\n");
    return builder.toString();
  }

  protected String formatXmlRecordHeader(
      String className, String recordId, String version, String instance) {
    StringBuilder builder = new StringBuilder();
    builder
        .append("<")
        .append(className)
        .append(" recordId=\"0x")
        .append(recordId)
        .append("\" version=\"0x")
        .append(version)
        .append("\" instance=\"0x")
        .append(instance)
        .append("\" size=\"")
        .append(getRecordSize())
        .append("\">\n");
    return builder.toString();
  }

  public String toXml() {
    return toXml("");
  }
}
Example #2
0
/**
 * "Special Attributes" This seems to be a Misc Stuff and Junk record. One function it serves is in
 * SUM functions (i.e. SUM(A1:A3) causes an area PTG then an ATTR with the SUM option set)
 *
 * @author andy
 * @author Jason Height (jheight at chariot dot net dot au)
 */
public final class AttrPtg extends ControlPtg {
  public static final byte sid = 0x19;
  private static final int SIZE = 4;
  private final byte _options;
  private final short _data;

  /** only used for tAttrChoose: table of offsets to starts of args */
  private final int[] _jumpTable;
  /** only used for tAttrChoose: offset to the tFuncVar for CHOOSE() */
  private final int _chooseFuncOffset;

  // flags 'volatile' and 'space', can be combined.
  // OOO spec says other combinations are theoretically possible but not likely to occur.
  private static final BitField semiVolatile = BitFieldFactory.getInstance(0x01);
  private static final BitField optiIf = BitFieldFactory.getInstance(0x02);
  private static final BitField optiChoose = BitFieldFactory.getInstance(0x04);
  private static final BitField optiSkip = BitFieldFactory.getInstance(0x08);
  private static final BitField optiSum = BitFieldFactory.getInstance(0x10);
  private static final BitField baxcel =
      BitFieldFactory.getInstance(0x20); // 'assignment-style formula in a macro sheet'
  private static final BitField space = BitFieldFactory.getInstance(0x40);

  public static final AttrPtg SUM = new AttrPtg(0x0010, 0, null, -1);

  public static final class SpaceType {
    private SpaceType() {
      // no instances of this class
    }

    /** 00H = Spaces before the next token (not allowed before tParen token) */
    public static final int SPACE_BEFORE = 0x00;
    /** 01H = Carriage returns before the next token (not allowed before tParen token) */
    public static final int CR_BEFORE = 0x01;
    /** 02H = Spaces before opening parenthesis (only allowed before tParen token) */
    public static final int SPACE_BEFORE_OPEN_PAREN = 0x02;
    /** 03H = Carriage returns before opening parenthesis (only allowed before tParen token) */
    public static final int CR_BEFORE_OPEN_PAREN = 0x03;
    /**
     * 04H = Spaces before closing parenthesis (only allowed before tParen, tFunc, and tFuncVar
     * tokens)
     */
    public static final int SPACE_BEFORE_CLOSE_PAREN = 0x04;
    /**
     * 05H = Carriage returns before closing parenthesis (only allowed before tParen, tFunc, and
     * tFuncVar tokens)
     */
    public static final int CR_BEFORE_CLOSE_PAREN = 0x05;
    /** 06H = Spaces following the equality sign (only in macro sheets) */
    public static final int SPACE_AFTER_EQUALITY = 0x06;
  }

  public AttrPtg(LittleEndianInput in) {
    _options = in.readByte();
    _data = in.readShort();
    if (isOptimizedChoose()) {
      int nCases = _data;
      int[] jumpTable = new int[nCases];
      for (int i = 0; i < jumpTable.length; i++) {
        jumpTable[i] = in.readUShort();
      }
      _jumpTable = jumpTable;
      _chooseFuncOffset = in.readUShort();
    } else {
      _jumpTable = null;
      _chooseFuncOffset = -1;
    }
  }

  private AttrPtg(int options, int data, int[] jt, int chooseFuncOffset) {
    _options = (byte) options;
    _data = (short) data;
    _jumpTable = jt;
    _chooseFuncOffset = chooseFuncOffset;
  }

  /**
   * @param type a constant from <tt>SpaceType</tt>
   * @param count the number of space characters
   */
  public static AttrPtg createSpace(int type, int count) {
    int data = type & 0x00FF | (count << 8) & 0x00FFFF;
    return new AttrPtg(space.set(0), data, null, -1);
  }

  /**
   * @param dist distance (in bytes) to start of either
   *     <ul>
   *       <li>false parameter
   *       <li>tFuncVar(IF) token (when false parameter is not present)
   *     </ul>
   */
  public static AttrPtg createIf(int dist) {
    return new AttrPtg(optiIf.set(0), dist, null, -1);
  }

  /** @param dist distance (in bytes) to position behind tFuncVar(IF) token (minus 1) */
  public static AttrPtg createSkip(int dist) {
    return new AttrPtg(optiSkip.set(0), dist, null, -1);
  }

  public static AttrPtg getSumSingle() {
    return new AttrPtg(optiSum.set(0), 0, null, -1);
  }

  public boolean isSemiVolatile() {
    return semiVolatile.isSet(_options);
  }

  public boolean isOptimizedIf() {
    return optiIf.isSet(_options);
  }

  public boolean isOptimizedChoose() {
    return optiChoose.isSet(_options);
  }

  public boolean isSum() {
    return optiSum.isSet(_options);
  }

  public boolean isSkip() {
    return optiSkip.isSet(_options);
  }

  // lets hope no one uses this anymore
  private boolean isBaxcel() {
    return baxcel.isSet(_options);
  }

  public boolean isSpace() {
    return space.isSet(_options);
  }

  public short getData() {
    return _data;
  }

  public int[] getJumpTable() {
    return _jumpTable.clone();
  }

  public int getChooseFuncOffset() {
    if (_jumpTable == null) {
      throw new IllegalStateException("Not tAttrChoose");
    }
    return _chooseFuncOffset;
  }

  public String toString() {
    StringBuffer sb = new StringBuffer(64);
    sb.append(getClass().getName()).append(" [");

    if (isSemiVolatile()) {
      sb.append("volatile ");
    }
    if (isSpace()) {
      sb.append("space count=").append((_data >> 8) & 0x00FF);
      sb.append(" type=").append(_data & 0x00FF).append(" ");
    }
    // the rest seem to be mutually exclusive
    if (isOptimizedIf()) {
      sb.append("if dist=").append(_data);
    } else if (isOptimizedChoose()) {
      sb.append("choose nCases=").append(_data);
    } else if (isSkip()) {
      sb.append("skip dist=").append(_data);
    } else if (isSum()) {
      sb.append("sum ");
    } else if (isBaxcel()) {
      sb.append("assign ");
    }
    sb.append("]");
    return sb.toString();
  }

  public void write(LittleEndianOutput out) {
    out.writeByte(sid + getPtgClass());
    out.writeByte(_options);
    out.writeShort(_data);
    int[] jt = _jumpTable;
    if (jt != null) {
      for (int i = 0; i < jt.length; i++) {
        out.writeShort(jt[i]);
      }
      out.writeShort(_chooseFuncOffset);
    }
  }

  public int getSize() {
    if (_jumpTable != null) {
      return SIZE + (_jumpTable.length + 1) * LittleEndian.SHORT_SIZE;
    }
    return SIZE;
  }

  public String toFormulaString(String[] operands) {
    if (space.isSet(_options)) {
      return operands[0];
    } else if (optiIf.isSet(_options)) {
      return toFormulaString() + "(" + operands[0] + ")";
    } else if (optiSkip.isSet(_options)) {
      return toFormulaString()
          + operands[0]; // goto isn't a real formula element should not show up
    } else {
      return toFormulaString() + "(" + operands[0] + ")";
    }
  }

  public int getNumberOfOperands() {
    return 1;
  }

  public int getType() {
    return -1;
  }

  public String toFormulaString() {
    if (semiVolatile.isSet(_options)) {
      return "ATTR(semiVolatile)";
    }
    if (optiIf.isSet(_options)) {
      return "IF";
    }
    if (optiChoose.isSet(_options)) {
      return "CHOOSE";
    }
    if (optiSkip.isSet(_options)) {
      return "";
    }
    if (optiSum.isSet(_options)) {
      return "SUM";
    }
    if (baxcel.isSet(_options)) {
      return "ATTR(baxcel)";
    }
    if (space.isSet(_options)) {
      return "";
    }
    return "UNKNOWN ATTRIBUTE";
  }
}
/**
 * Table Cell Descriptor. NOTE: This source is automatically generated please do not modify this
 * file. Either subclass or remove the record in src/records/definitions.
 *
 * @author S. Ryan Ackley
 */
public abstract class TCAbstractType implements HDFType {

  protected short field_1_rgf;
  private static BitField fFirstMerged = BitFieldFactory.getInstance(0x0001);
  private static BitField fMerged = BitFieldFactory.getInstance(0x0002);
  private static BitField fVertical = BitFieldFactory.getInstance(0x0004);
  private static BitField fBackward = BitFieldFactory.getInstance(0x0008);
  private static BitField fRotateFont = BitFieldFactory.getInstance(0x0010);
  private static BitField fVertMerge = BitFieldFactory.getInstance(0x0020);
  private static BitField fVertRestart = BitFieldFactory.getInstance(0x0040);
  private static BitField vertAlign = BitFieldFactory.getInstance(0x0180);
  protected short field_2_unused;
  protected BorderCode field_3_brcTop;
  protected BorderCode field_4_brcLeft;
  protected BorderCode field_5_brcBottom;
  protected BorderCode field_6_brcRight;

  public TCAbstractType() {}

  protected void fillFields(byte[] data, int offset) {
    field_1_rgf = LittleEndian.getShort(data, 0x0 + offset);
    field_2_unused = LittleEndian.getShort(data, 0x2 + offset);
    field_3_brcTop = new BorderCode(data, 0x4 + offset);
    field_4_brcLeft = new BorderCode(data, 0x8 + offset);
    field_5_brcBottom = new BorderCode(data, 0xc + offset);
    field_6_brcRight = new BorderCode(data, 0x10 + offset);
  }

  public void serialize(byte[] data, int offset) {
    LittleEndian.putShort(data, 0x0 + offset, field_1_rgf);
    LittleEndian.putShort(data, 0x2 + offset, field_2_unused);
    field_3_brcTop.serialize(data, 0x4 + offset);
    field_4_brcLeft.serialize(data, 0x8 + offset);
    field_5_brcBottom.serialize(data, 0xc + offset);
    field_6_brcRight.serialize(data, 0x10 + offset);
  }

  public String toString() {
    StringBuffer buffer = new StringBuffer();

    buffer.append("[TC]\n");

    buffer.append("    .rgf                  = ");
    buffer.append(" (").append(getRgf()).append(" )\n");
    buffer.append("         .fFirstMerged             = ").append(isFFirstMerged()).append('\n');
    buffer.append("         .fMerged                  = ").append(isFMerged()).append('\n');
    buffer.append("         .fVertical                = ").append(isFVertical()).append('\n');
    buffer.append("         .fBackward                = ").append(isFBackward()).append('\n');
    buffer.append("         .fRotateFont              = ").append(isFRotateFont()).append('\n');
    buffer.append("         .fVertMerge               = ").append(isFVertMerge()).append('\n');
    buffer.append("         .fVertRestart             = ").append(isFVertRestart()).append('\n');
    buffer.append("         .vertAlign                = ").append(getVertAlign()).append('\n');

    buffer.append("    .unused               = ");
    buffer.append(" (").append(getUnused()).append(" )\n");

    buffer.append("    .brcTop               = ");
    buffer.append(" (").append(getBrcTop()).append(" )\n");

    buffer.append("    .brcLeft              = ");
    buffer.append(" (").append(getBrcLeft()).append(" )\n");

    buffer.append("    .brcBottom            = ");
    buffer.append(" (").append(getBrcBottom()).append(" )\n");

    buffer.append("    .brcRight             = ");
    buffer.append(" (").append(getBrcRight()).append(" )\n");

    buffer.append("[/TC]\n");
    return buffer.toString();
  }

  /** Size of record (exluding 4 byte header) */
  public int getSize() {
    return 4 + +2 + 2 + 4 + 4 + 4 + 4;
  }

  /** Get the rgf field for the TC record. */
  public short getRgf() {
    return field_1_rgf;
  }

  /** Set the rgf field for the TC record. */
  public void setRgf(short field_1_rgf) {
    this.field_1_rgf = field_1_rgf;
  }

  /** Get the unused field for the TC record. */
  public short getUnused() {
    return field_2_unused;
  }

  /** Set the unused field for the TC record. */
  public void setUnused(short field_2_unused) {
    this.field_2_unused = field_2_unused;
  }

  /** Get the brcTop field for the TC record. */
  public BorderCode getBrcTop() {
    return field_3_brcTop;
  }

  /** Set the brcTop field for the TC record. */
  public void setBrcTop(BorderCode field_3_brcTop) {
    this.field_3_brcTop = field_3_brcTop;
  }

  /** Get the brcLeft field for the TC record. */
  public BorderCode getBrcLeft() {
    return field_4_brcLeft;
  }

  /** Set the brcLeft field for the TC record. */
  public void setBrcLeft(BorderCode field_4_brcLeft) {
    this.field_4_brcLeft = field_4_brcLeft;
  }

  /** Get the brcBottom field for the TC record. */
  public BorderCode getBrcBottom() {
    return field_5_brcBottom;
  }

  /** Set the brcBottom field for the TC record. */
  public void setBrcBottom(BorderCode field_5_brcBottom) {
    this.field_5_brcBottom = field_5_brcBottom;
  }

  /** Get the brcRight field for the TC record. */
  public BorderCode getBrcRight() {
    return field_6_brcRight;
  }

  /** Set the brcRight field for the TC record. */
  public void setBrcRight(BorderCode field_6_brcRight) {
    this.field_6_brcRight = field_6_brcRight;
  }

  /** Sets the fFirstMerged field value. */
  public void setFFirstMerged(boolean value) {
    field_1_rgf = (short) fFirstMerged.setBoolean(field_1_rgf, value);
  }

  /** @return the fFirstMerged field value. */
  public boolean isFFirstMerged() {
    return fFirstMerged.isSet(field_1_rgf);
  }

  /** Sets the fMerged field value. */
  public void setFMerged(boolean value) {
    field_1_rgf = (short) fMerged.setBoolean(field_1_rgf, value);
  }

  /** @return the fMerged field value. */
  public boolean isFMerged() {
    return fMerged.isSet(field_1_rgf);
  }

  /** Sets the fVertical field value. */
  public void setFVertical(boolean value) {
    field_1_rgf = (short) fVertical.setBoolean(field_1_rgf, value);
  }

  /** @return the fVertical field value. */
  public boolean isFVertical() {
    return fVertical.isSet(field_1_rgf);
  }

  /** Sets the fBackward field value. */
  public void setFBackward(boolean value) {
    field_1_rgf = (short) fBackward.setBoolean(field_1_rgf, value);
  }

  /** @return the fBackward field value. */
  public boolean isFBackward() {
    return fBackward.isSet(field_1_rgf);
  }

  /** Sets the fRotateFont field value. */
  public void setFRotateFont(boolean value) {
    field_1_rgf = (short) fRotateFont.setBoolean(field_1_rgf, value);
  }

  /** @return the fRotateFont field value. */
  public boolean isFRotateFont() {
    return fRotateFont.isSet(field_1_rgf);
  }

  /** Sets the fVertMerge field value. */
  public void setFVertMerge(boolean value) {
    field_1_rgf = (short) fVertMerge.setBoolean(field_1_rgf, value);
  }

  /** @return the fVertMerge field value. */
  public boolean isFVertMerge() {
    return fVertMerge.isSet(field_1_rgf);
  }

  /** Sets the fVertRestart field value. */
  public void setFVertRestart(boolean value) {
    field_1_rgf = (short) fVertRestart.setBoolean(field_1_rgf, value);
  }

  /** @return the fVertRestart field value. */
  public boolean isFVertRestart() {
    return fVertRestart.isSet(field_1_rgf);
  }

  /** Sets the vertAlign field value. */
  public void setVertAlign(byte value) {
    field_1_rgf = (short) vertAlign.setValue(field_1_rgf, value);
  }

  /** @return the vertAlign field value. */
  public byte getVertAlign() {
    return (byte) vertAlign.getValue(field_1_rgf);
  }
}
Example #4
0
/**
 * This data structure is used by a paragraph to determine how it should drop its first letter. I
 * think its the visual effect that will show a giant first letter to a paragraph. I've seen this
 * used in the first paragraph of a book
 *
 * @author Ryan Ackley
 */
public final class DropCapSpecifier implements Cloneable {
  private short _fdct;
  private static BitField _lines = BitFieldFactory.getInstance(0xf8);
  private static BitField _type = BitFieldFactory.getInstance(0x07);

  public DropCapSpecifier() {
    this._fdct = 0;
  }

  public DropCapSpecifier(byte[] buf, int offset) {
    this(LittleEndian.getShort(buf, offset));
  }

  public DropCapSpecifier(short fdct) {
    this._fdct = fdct;
  }

  @Override
  public DropCapSpecifier clone() {
    return new DropCapSpecifier(_fdct);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    DropCapSpecifier other = (DropCapSpecifier) obj;
    if (_fdct != other._fdct) return false;
    return true;
  }

  public byte getCountOfLinesToDrop() {
    return (byte) _lines.getValue(_fdct);
  }

  public byte getDropCapType() {
    return (byte) _type.getValue(_fdct);
  }

  @Override
  public int hashCode() {
    return _fdct;
  }

  public boolean isEmpty() {
    return _fdct == 0;
  }

  public void setCountOfLinesToDrop(byte value) {
    _fdct = (short) _lines.setValue(_fdct, value);
  }

  public void setDropCapType(byte value) {
    _fdct = (short) _type.setValue(_fdct, value);
  }

  public short toShort() {
    return _fdct;
  }

  @Override
  public String toString() {
    if (isEmpty()) return "[DCS] EMPTY";

    return "[DCS] (type: " + getDropCapType() + "; count: " + getCountOfLinesToDrop() + ")";
  }
}
/**
 * The TXO record (0x01B6) is used to define the properties of a text box. It is followed by two or
 * more continue records unless there is no actual text. The first continue records contain the text
 * data and the last continue record contains the formatting runs.
 *
 * <p>
 *
 * @author Glen Stampoultzis (glens at apache.org)
 */
public final class TextObjectRecord extends ContinuableRecord {
  public static final short sid = 0x01B6;

  private static final int FORMAT_RUN_ENCODED_SIZE = 8; // 2 shorts and 4 bytes reserved

  private static final BitField HorizontalTextAlignment = BitFieldFactory.getInstance(0x000E);
  private static final BitField VerticalTextAlignment = BitFieldFactory.getInstance(0x0070);
  private static final BitField textLocked = BitFieldFactory.getInstance(0x0200);

  public static final short HORIZONTAL_TEXT_ALIGNMENT_LEFT_ALIGNED = 1;
  public static final short HORIZONTAL_TEXT_ALIGNMENT_CENTERED = 2;
  public static final short HORIZONTAL_TEXT_ALIGNMENT_RIGHT_ALIGNED = 3;
  public static final short HORIZONTAL_TEXT_ALIGNMENT_JUSTIFIED = 4;
  public static final short VERTICAL_TEXT_ALIGNMENT_TOP = 1;
  public static final short VERTICAL_TEXT_ALIGNMENT_CENTER = 2;
  public static final short VERTICAL_TEXT_ALIGNMENT_BOTTOM = 3;
  public static final short VERTICAL_TEXT_ALIGNMENT_JUSTIFY = 4;

  public static final short TEXT_ORIENTATION_NONE = 0;
  public static final short TEXT_ORIENTATION_TOP_TO_BOTTOM = 1;
  public static final short TEXT_ORIENTATION_ROT_RIGHT = 2;
  public static final short TEXT_ORIENTATION_ROT_LEFT = 3;

  private int field_1_options;
  private int field_2_textOrientation;
  private int field_3_reserved4;
  private int field_4_reserved5;
  private int field_5_reserved6;
  private int field_8_reserved7;

  private HSSFRichTextString _text;

  /*
   * Note - the next three fields are very similar to those on
   * EmbededObjectRefSubRecord(ftPictFmla 0x0009)
   *
   * some observed values for the 4 bytes preceding the formula: C0 5E 86 03
   * C0 11 AC 02 80 F1 8A 03 D4 F0 8A 03
   */
  private int _unknownPreFormulaInt;
  /** expect tRef, tRef3D, tArea, tArea3D or tName */
  private OperandPtg _linkRefPtg;
  /**
   * Not clear if needed . Excel seems to be OK if this byte is not present. Value is often the same
   * as the earlier firstColumn byte.
   */
  private Byte _unknownPostFormulaByte;

  public TextObjectRecord() {
    //
  }

  public TextObjectRecord(RecordInputStream in) {
    field_1_options = in.readUShort();
    field_2_textOrientation = in.readUShort();
    field_3_reserved4 = in.readUShort();
    field_4_reserved5 = in.readUShort();
    field_5_reserved6 = in.readUShort();
    int field_6_textLength = in.readUShort();
    int field_7_formattingDataLength = in.readUShort();
    field_8_reserved7 = in.readInt();

    if (in.remaining() > 0) {
      // Text Objects can have simple reference formulas
      // (This bit not mentioned in the MS document)
      if (in.remaining() < 11) {
        throw new RecordFormatException("Not enough remaining data for a link formula");
      }
      int formulaSize = in.readUShort();
      _unknownPreFormulaInt = in.readInt();
      Ptg[] ptgs = Ptg.readTokens(formulaSize, in);
      if (ptgs.length != 1) {
        throw new RecordFormatException("Read " + ptgs.length + " tokens but expected exactly 1");
      }
      _linkRefPtg = (OperandPtg) ptgs[0];
      if (in.remaining() > 0) {
        _unknownPostFormulaByte = Byte.valueOf(in.readByte());
      } else {
        _unknownPostFormulaByte = null;
      }
    } else {
      _linkRefPtg = null;
    }
    if (in.remaining() > 0) {
      throw new RecordFormatException("Unused " + in.remaining() + " bytes at end of record");
    }

    String text;
    if (field_6_textLength > 0) {
      text = readRawString(in, field_6_textLength);
    } else {
      text = "";
    }
    _text = new HSSFRichTextString(text);

    if (field_7_formattingDataLength > 0) {
      processFontRuns(in, _text, field_7_formattingDataLength);
    }
  }

  private static String readRawString(RecordInputStream in, int textLength) {
    byte compressByte = in.readByte();
    boolean isCompressed = (compressByte & 0x01) == 0;
    if (isCompressed) {
      return in.readCompressedUnicode(textLength);
    }
    return in.readUnicodeLEString(textLength);
  }

  private static void processFontRuns(
      RecordInputStream in, HSSFRichTextString str, int formattingRunDataLength) {
    if (formattingRunDataLength % FORMAT_RUN_ENCODED_SIZE != 0) {
      throw new RecordFormatException(
          "Bad format run data length " + formattingRunDataLength + ")");
    }
    int nRuns = formattingRunDataLength / FORMAT_RUN_ENCODED_SIZE;
    for (int i = 0; i < nRuns; i++) {
      short index = in.readShort();
      short iFont = in.readShort();
      in.readInt(); // skip reserved.
      str.applyFont(index, str.length(), iFont);
    }
  }

  public short getSid() {
    return sid;
  }

  private void serializeTXORecord(ContinuableRecordOutput out) {

    out.writeShort(field_1_options);
    out.writeShort(field_2_textOrientation);
    out.writeShort(field_3_reserved4);
    out.writeShort(field_4_reserved5);
    out.writeShort(field_5_reserved6);
    out.writeShort(_text.length());
    out.writeShort(getFormattingDataLength());
    out.writeInt(field_8_reserved7);

    if (_linkRefPtg != null) {
      int formulaSize = _linkRefPtg.getSize();
      out.writeShort(formulaSize);
      out.writeInt(_unknownPreFormulaInt);
      _linkRefPtg.write(out);
      if (_unknownPostFormulaByte != null) {
        out.writeByte(_unknownPostFormulaByte.byteValue());
      }
    }
  }

  private void serializeTrailingRecords(ContinuableRecordOutput out) {
    out.writeContinue();
    out.writeStringData(_text.getString());
    out.writeContinue();
    writeFormatData(out, _text);
  }

  protected void serialize(ContinuableRecordOutput out) {

    serializeTXORecord(out);
    if (_text.getString().length() > 0) {
      serializeTrailingRecords(out);
    }
  }

  private int getFormattingDataLength() {
    if (_text.length() < 1) {
      // important - no formatting data if text is empty
      return 0;
    }
    return (_text.numFormattingRuns() + 1) * FORMAT_RUN_ENCODED_SIZE;
  }

  private static void writeFormatData(ContinuableRecordOutput out, HSSFRichTextString str) {
    int nRuns = str.numFormattingRuns();
    for (int i = 0; i < nRuns; i++) {
      out.writeShort(str.getIndexOfFormattingRun(i));
      int fontIndex = str.getFontOfFormattingRun(i);
      out.writeShort(fontIndex == str.NO_FONT ? 0 : fontIndex);
      out.writeInt(0); // skip reserved
    }
    out.writeShort(str.length());
    out.writeShort(0);
    out.writeInt(0); // skip reserved
  }

  /** Sets the Horizontal text alignment field value. */
  public void setHorizontalTextAlignment(int value) {
    field_1_options = HorizontalTextAlignment.setValue(field_1_options, value);
  }

  /** @return the Horizontal text alignment field value. */
  public int getHorizontalTextAlignment() {
    return HorizontalTextAlignment.getValue(field_1_options);
  }

  /** Sets the Vertical text alignment field value. */
  public void setVerticalTextAlignment(int value) {
    field_1_options = VerticalTextAlignment.setValue(field_1_options, value);
  }

  /** @return the Vertical text alignment field value. */
  public int getVerticalTextAlignment() {
    return VerticalTextAlignment.getValue(field_1_options);
  }

  /** Sets the text locked field value. */
  public void setTextLocked(boolean value) {
    field_1_options = textLocked.setBoolean(field_1_options, value);
  }

  /** @return the text locked field value. */
  public boolean isTextLocked() {
    return textLocked.isSet(field_1_options);
  }

  /**
   * Get the text orientation field for the TextObjectBase record.
   *
   * @return One of TEXT_ORIENTATION_NONE TEXT_ORIENTATION_TOP_TO_BOTTOM TEXT_ORIENTATION_ROT_RIGHT
   *     TEXT_ORIENTATION_ROT_LEFT
   */
  public int getTextOrientation() {
    return field_2_textOrientation;
  }

  /**
   * Set the text orientation field for the TextObjectBase record.
   *
   * @param textOrientation One of TEXT_ORIENTATION_NONE TEXT_ORIENTATION_TOP_TO_BOTTOM
   *     TEXT_ORIENTATION_ROT_RIGHT TEXT_ORIENTATION_ROT_LEFT
   */
  public void setTextOrientation(int textOrientation) {
    this.field_2_textOrientation = textOrientation;
  }

  public HSSFRichTextString getStr() {
    return _text;
  }

  public void setStr(HSSFRichTextString str) {
    _text = str;
  }

  public Ptg getLinkRefPtg() {
    return _linkRefPtg;
  }

  public String toString() {
    StringBuffer sb = new StringBuffer();

    sb.append("[TXO]\n");
    sb.append("    .options        = ").append(HexDump.shortToHex(field_1_options)).append("\n");
    sb.append("         .isHorizontal = ").append(getHorizontalTextAlignment()).append('\n');
    sb.append("         .isVertical   = ").append(getVerticalTextAlignment()).append('\n');
    sb.append("         .textLocked   = ").append(isTextLocked()).append('\n');
    sb.append("    .textOrientation= ")
        .append(HexDump.shortToHex(getTextOrientation()))
        .append("\n");
    sb.append("    .reserved4      = ").append(HexDump.shortToHex(field_3_reserved4)).append("\n");
    sb.append("    .reserved5      = ").append(HexDump.shortToHex(field_4_reserved5)).append("\n");
    sb.append("    .reserved6      = ").append(HexDump.shortToHex(field_5_reserved6)).append("\n");
    sb.append("    .textLength     = ").append(HexDump.shortToHex(_text.length())).append("\n");
    sb.append("    .reserved7      = ").append(HexDump.intToHex(field_8_reserved7)).append("\n");

    sb.append("    .string = ").append(_text).append('\n');

    for (int i = 0; i < _text.numFormattingRuns(); i++) {
      sb.append("    .textrun = ").append(_text.getFontOfFormattingRun(i)).append('\n');
    }
    sb.append("[/TXO]\n");
    return sb.toString();
  }

  public Object clone() {

    TextObjectRecord rec = new TextObjectRecord();
    rec._text = _text;

    rec.field_1_options = field_1_options;
    rec.field_2_textOrientation = field_2_textOrientation;
    rec.field_3_reserved4 = field_3_reserved4;
    rec.field_4_reserved5 = field_4_reserved5;
    rec.field_5_reserved6 = field_5_reserved6;
    rec.field_8_reserved7 = field_8_reserved7;

    rec._text = _text; // clone needed?

    if (_linkRefPtg != null) {
      rec._unknownPreFormulaInt = _unknownPreFormulaInt;
      rec._linkRefPtg = _linkRefPtg.copy();
      rec._unknownPostFormulaByte = _unknownPostFormulaByte;
    }
    return rec;
  }
}
/**
 * Formula Record (0x0006). REFERENCE: PG 317/444 Microsoft Excel 97 Developer's Kit (ISBN:
 * 1-57231-498-2)
 *
 * <p>
 *
 * @author Andrew C. Oliver (acoliver at apache dot org)
 * @author Jason Height (jheight at chariot dot net dot au)
 */
public final class FormulaRecord extends CellRecord {

  public static final short sid =
      0x0006; // docs say 406...because of a bug Microsoft support site article #Q184647)
  private static int FIXED_SIZE = 14; // double + short + int

  private static final BitField alwaysCalc = BitFieldFactory.getInstance(0x0001);
  private static final BitField calcOnLoad = BitFieldFactory.getInstance(0x0002);
  private static final BitField sharedFormula = BitFieldFactory.getInstance(0x0008);

  /**
   * Manages the cached formula result values of other types besides numeric. Excel encodes the same
   * 8 bytes that would be field_4_value with various NaN values that are decoded/encoded by this
   * class.
   */
  private static final class SpecialCachedValue {
    /** deliberately chosen by Excel in order to encode other values within Double NaNs */
    private static final long BIT_MARKER = 0xFFFF000000000000L;

    private static final int VARIABLE_DATA_LENGTH = 6;
    private static final int DATA_INDEX = 2;

    public static final int STRING = 0;
    public static final int BOOLEAN = 1;
    public static final int ERROR_CODE = 2;
    public static final int EMPTY = 3;

    private final byte[] _variableData;

    private SpecialCachedValue(byte[] data) {
      _variableData = data;
    }

    public int getTypeCode() {
      return _variableData[0];
    }

    /**
     * @return <code>null</code> if the double value encoded by <tt>valueLongBits</tt> is a normal
     *     (non NaN) double value.
     */
    public static SpecialCachedValue create(long valueLongBits) {
      if ((BIT_MARKER & valueLongBits) != BIT_MARKER) {
        return null;
      }

      byte[] result = new byte[VARIABLE_DATA_LENGTH];
      long x = valueLongBits;
      for (int i = 0; i < VARIABLE_DATA_LENGTH; i++) {
        result[i] = (byte) x;
        x >>= 8;
      }
      switch (result[0]) {
        case STRING:
        case BOOLEAN:
        case ERROR_CODE:
        case EMPTY:
          break;
        default:
          throw new RecordFormatException("Bad special value code (" + result[0] + ")");
      }
      return new SpecialCachedValue(result);
    }

    public void serialize(LittleEndianOutput out) {
      out.write(_variableData);
      out.writeShort(0xFFFF);
    }

    public String formatDebugString() {
      return formatValue() + ' ' + HexDump.toHex(_variableData);
    }

    private String formatValue() {
      int typeCode = getTypeCode();
      switch (typeCode) {
        case STRING:
          return "<string>";
        case BOOLEAN:
          return getDataValue() == 0 ? "FALSE" : "TRUE";
        case ERROR_CODE:
          return ErrorEval.getText(getDataValue());
        case EMPTY:
          return "<empty>";
      }
      return "#error(type=" + typeCode + ")#";
    }

    private int getDataValue() {
      return _variableData[DATA_INDEX];
    }

    public static SpecialCachedValue createCachedEmptyValue() {
      return create(EMPTY, 0);
    }

    public static SpecialCachedValue createForString() {
      return create(STRING, 0);
    }

    public static SpecialCachedValue createCachedBoolean(boolean b) {
      return create(BOOLEAN, b ? 1 : 0);
    }

    public static SpecialCachedValue createCachedErrorCode(int errorCode) {
      return create(ERROR_CODE, errorCode);
    }

    private static SpecialCachedValue create(int code, int data) {
      byte[] vd = {
        (byte) code, 0, (byte) data, 0, 0, 0,
      };
      return new SpecialCachedValue(vd);
    }

    public String toString() {
      StringBuffer sb = new StringBuffer(64);
      sb.append(getClass().getName());
      sb.append('[').append(formatValue()).append(']');
      return sb.toString();
    }

    public int getValueType() {
      int typeCode = getTypeCode();
      switch (typeCode) {
        case STRING:
          return HSSFCell.CELL_TYPE_STRING;
        case BOOLEAN:
          return HSSFCell.CELL_TYPE_BOOLEAN;
        case ERROR_CODE:
          return HSSFCell.CELL_TYPE_ERROR;
        case EMPTY:
          return HSSFCell.CELL_TYPE_STRING; // is this correct?
      }
      throw new IllegalStateException("Unexpected type id (" + typeCode + ")");
    }

    public boolean getBooleanValue() {
      if (getTypeCode() != BOOLEAN) {
        throw new IllegalStateException("Not a boolean cached value - " + formatValue());
      }
      return getDataValue() != 0;
    }

    public int getErrorValue() {
      if (getTypeCode() != ERROR_CODE) {
        throw new IllegalStateException("Not an error cached value - " + formatValue());
      }
      return getDataValue();
    }
  }

  private double field_4_value;
  private short field_5_options;
  /**
   * Unused field. As it turns out this field is often not zero.. According to Microsoft Excel
   * Developer's Kit Page 318: when writing the chn field (offset 20), it's supposed to be 0 but
   * ignored on read
   */
  private int field_6_zero;

  private Formula field_8_parsed_expr;

  /**
   * Since the NaN support seems sketchy (different constants) we'll store and spit it out directly
   */
  private SpecialCachedValue specialCachedValue;

  /** Creates new FormulaRecord */
  public FormulaRecord() {
    field_8_parsed_expr = Formula.create(Ptg.EMPTY_PTG_ARRAY);
  }

  public FormulaRecord(RecordInputStream ris) {
    super(ris);
    LittleEndianInput in = ris;
    long valueLongBits = in.readLong();
    field_5_options = in.readShort();
    specialCachedValue = SpecialCachedValue.create(valueLongBits);
    if (specialCachedValue == null) {
      field_4_value = Double.longBitsToDouble(valueLongBits);
    }

    field_6_zero = in.readInt();

    int field_7_expression_len =
        in.readShort(); // this length does not include any extra array data
    int nBytesAvailable = in.available();
    field_8_parsed_expr = Formula.read(field_7_expression_len, in, nBytesAvailable);
  }

  /**
   * set the calculated value of the formula
   *
   * @param value calculated value
   */
  public void setValue(double value) {
    field_4_value = value;
    specialCachedValue = null;
  }

  public void setCachedResultTypeEmptyString() {
    specialCachedValue = SpecialCachedValue.createCachedEmptyValue();
  }

  public void setCachedResultTypeString() {
    specialCachedValue = SpecialCachedValue.createForString();
  }

  public void setCachedResultErrorCode(int errorCode) {
    specialCachedValue = SpecialCachedValue.createCachedErrorCode(errorCode);
  }

  public void setCachedResultBoolean(boolean value) {
    specialCachedValue = SpecialCachedValue.createCachedBoolean(value);
  }
  /**
   * @return <code>true</code> if this {@link FormulaRecord} is followed by a {@link StringRecord}
   *     representing the cached text result of the formula evaluation.
   */
  public boolean hasCachedResultString() {
    if (specialCachedValue == null) {
      return false;
    }
    return specialCachedValue.getTypeCode() == SpecialCachedValue.STRING;
  }

  public int getCachedResultType() {
    if (specialCachedValue == null) {
      return HSSFCell.CELL_TYPE_NUMERIC;
    }
    return specialCachedValue.getValueType();
  }

  public boolean getCachedBooleanValue() {
    return specialCachedValue.getBooleanValue();
  }

  public int getCachedErrorValue() {
    return specialCachedValue.getErrorValue();
  }

  /**
   * set the option flags
   *
   * @param options bitmask
   */
  public void setOptions(short options) {
    field_5_options = options;
  }

  /**
   * get the calculated value of the formula
   *
   * @return calculated value
   */
  public double getValue() {
    return field_4_value;
  }

  /**
   * get the option flags
   *
   * @return bitmask
   */
  public short getOptions() {
    return field_5_options;
  }

  public boolean isSharedFormula() {
    return sharedFormula.isSet(field_5_options);
  }

  public void setSharedFormula(boolean flag) {
    field_5_options = sharedFormula.setShortBoolean(field_5_options, flag);
  }

  public boolean isAlwaysCalc() {
    return alwaysCalc.isSet(field_5_options);
  }

  public void setAlwaysCalc(boolean flag) {
    field_5_options = alwaysCalc.setShortBoolean(field_5_options, flag);
  }

  public boolean isCalcOnLoad() {
    return calcOnLoad.isSet(field_5_options);
  }

  public void setCalcOnLoad(boolean flag) {
    field_5_options = calcOnLoad.setShortBoolean(field_5_options, flag);
  }

  /** @return the formula tokens. never <code>null</code> */
  public Ptg[] getParsedExpression() {
    return field_8_parsed_expr.getTokens();
  }

  public Formula getFormula() {
    return field_8_parsed_expr;
  }

  public void setParsedExpression(Ptg[] ptgs) {
    field_8_parsed_expr = Formula.create(ptgs);
  }

  public short getSid() {
    return sid;
  }

  @Override
  protected int getValueDataSize() {
    return FIXED_SIZE + field_8_parsed_expr.getEncodedSize();
  }

  @Override
  protected void serializeValue(LittleEndianOutput out) {

    if (specialCachedValue == null) {
      out.writeDouble(field_4_value);
    } else {
      specialCachedValue.serialize(out);
    }

    out.writeShort(getOptions());

    out.writeInt(
        field_6_zero); // may as well write original data back so as to minimise differences from
                       // original
    field_8_parsed_expr.serialize(out);
  }

  @Override
  protected String getRecordName() {
    return "FORMULA";
  }

  @Override
  protected void appendValueText(StringBuilder sb) {
    sb.append("  .value	 = ");
    if (specialCachedValue == null) {
      sb.append(field_4_value).append("\n");
    } else {
      sb.append(specialCachedValue.formatDebugString()).append("\n");
    }
    sb.append("  .options   = ").append(HexDump.shortToHex(getOptions())).append("\n");
    sb.append("    .alwaysCalc= ").append(isAlwaysCalc()).append("\n");
    sb.append("    .calcOnLoad= ").append(isCalcOnLoad()).append("\n");
    sb.append("    .shared    = ").append(isSharedFormula()).append("\n");
    sb.append("  .zero      = ").append(HexDump.intToHex(field_6_zero)).append("\n");

    Ptg[] ptgs = field_8_parsed_expr.getTokens();
    for (int k = 0; k < ptgs.length; k++) {
      if (k > 0) {
        sb.append("\n");
      }
      sb.append("    Ptg[").append(k).append("]=");
      Ptg ptg = ptgs[k];
      sb.append(ptg.toString()).append(ptg.getRVAType());
    }
  }

  public Object clone() {
    FormulaRecord rec = new FormulaRecord();
    copyBaseFields(rec);
    rec.field_4_value = field_4_value;
    rec.field_5_options = field_5_options;
    rec.field_6_zero = field_6_zero;
    rec.field_8_parsed_expr = field_8_parsed_expr;
    rec.specialCachedValue = specialCachedValue;
    return rec;
  }
}
Example #7
0
/**
 * Specifies a rectangular area of cells A1:A4 for instance.
 *
 * @author andy
 * @author Jason Height (jheight at chariot dot net dot au)
 */
public abstract class AreaPtgBase extends OperandPtg implements AreaI {
  /**
   * TODO - (May-2008) fix subclasses of AreaPtg 'AreaN~' which are used in shared formulas. see
   * similar comment in ReferencePtg
   */
  protected final RuntimeException notImplemented() {
    return new RuntimeException(
        "Coding Error: This method should never be called. This ptg should be converted");
  }

  /** zero based, unsigned 16 bit */
  private int field_1_first_row;
  /** zero based, unsigned 16 bit */
  private int field_2_last_row;
  /** zero based, unsigned 8 bit */
  private int
      field_3_first_column; // BitFields: (first row relative, first col relative, first column
  // number)
  /** zero based, unsigned 8 bit */
  private int
      field_4_last_column; // BitFields: (last row relative, last col relative, last column number)

  private static final BitField rowRelative = BitFieldFactory.getInstance(0x8000);
  private static final BitField colRelative = BitFieldFactory.getInstance(0x4000);
  private static final BitField columnMask = BitFieldFactory.getInstance(0x3FFF);

  protected AreaPtgBase() {
    // do nothing
  }

  protected AreaPtgBase(AreaReference ar) {
    CellReference firstCell = ar.getFirstCell();
    CellReference lastCell = ar.getLastCell();
    setFirstRow(firstCell.getRow());
    setFirstColumn(firstCell.getCol() == -1 ? 0 : firstCell.getCol());
    setLastRow(lastCell.getRow());
    setLastColumn(lastCell.getCol() == -1 ? 0xFF : lastCell.getCol());
    setFirstColRelative(!firstCell.isColAbsolute());
    setLastColRelative(!lastCell.isColAbsolute());
    setFirstRowRelative(!firstCell.isRowAbsolute());
    setLastRowRelative(!lastCell.isRowAbsolute());
  }

  protected AreaPtgBase(
      int firstRow,
      int lastRow,
      int firstColumn,
      int lastColumn,
      boolean firstRowRelative,
      boolean lastRowRelative,
      boolean firstColRelative,
      boolean lastColRelative) {

    if (lastRow >= firstRow) {
      setFirstRow(firstRow);
      setLastRow(lastRow);
      setFirstRowRelative(firstRowRelative);
      setLastRowRelative(lastRowRelative);
    } else {
      setFirstRow(lastRow);
      setLastRow(firstRow);
      setFirstRowRelative(lastRowRelative);
      setLastRowRelative(firstRowRelative);
    }

    if (lastColumn >= firstColumn) {
      setFirstColumn(firstColumn);
      setLastColumn(lastColumn);
      setFirstColRelative(firstColRelative);
      setLastColRelative(lastColRelative);
    } else {
      setFirstColumn(lastColumn);
      setLastColumn(firstColumn);
      setFirstColRelative(lastColRelative);
      setLastColRelative(firstColRelative);
    }
  }

  /**
   * Sort the first and last row and columns in-place to the preferred (top left:bottom right) order
   * Note: Sort only occurs when an instance is constructed or when this method is called.
   *
   * <p>For example, <code>$E5:B$10</code> becomes <code>B5:$E$10</code>
   */
  public void sortTopLeftToBottomRight() {
    if (getFirstRow() > getLastRow()) {
      // swap first row and last row numbers and relativity
      // Note: cannot just swap the fields because row relativity is stored in fields 3 and 4
      final int firstRow = getFirstRow();
      final boolean firstRowRel = isFirstRowRelative();
      setFirstRow(getLastRow());
      setFirstRowRelative(isLastRowRelative());
      setLastRow(firstRow);
      setLastRowRelative(firstRowRel);
    }
    if (getFirstColumn() > getLastColumn()) {
      // swap first column and last column numbers and relativity
      // Note: cannot just swap the fields because row relativity is stored in fields 3 and 4
      final int firstCol = getFirstColumn();
      final boolean firstColRel = isFirstColRelative();
      setFirstColumn(getLastColumn());
      setFirstColRelative(isLastColRelative());
      setLastColumn(firstCol);
      setLastColRelative(firstColRel);
    }
  }

  protected final void readCoordinates(LittleEndianInput in) {
    field_1_first_row = in.readUShort();
    field_2_last_row = in.readUShort();
    field_3_first_column = in.readUShort();
    field_4_last_column = in.readUShort();
  }

  protected final void writeCoordinates(LittleEndianOutput out) {
    out.writeShort(field_1_first_row);
    out.writeShort(field_2_last_row);
    out.writeShort(field_3_first_column);
    out.writeShort(field_4_last_column);
  }

  /** @return the first row in the area */
  public final int getFirstRow() {
    return field_1_first_row;
  }

  /**
   * sets the first row
   *
   * @param rowIx number (0-based)
   */
  public final void setFirstRow(int rowIx) {
    field_1_first_row = rowIx;
  }

  /** @return last row in the range (x2 in x1,y1-x2,y2) */
  public final int getLastRow() {
    return field_2_last_row;
  }

  /** @param rowIx last row number in the area */
  public final void setLastRow(int rowIx) {
    field_2_last_row = rowIx;
  }

  /** @return the first column number in the area. */
  public final int getFirstColumn() {
    return columnMask.getValue(field_3_first_column);
  }

  /** @return the first column number + the options bit settings unstripped */
  public final short getFirstColumnRaw() {
    return (short) field_3_first_column; // TODO
  }

  /** @return whether or not the first row is a relative reference or not. */
  public final boolean isFirstRowRelative() {
    return rowRelative.isSet(field_3_first_column);
  }

  /**
   * sets the first row to relative or not
   *
   * @param rel is relative or not.
   */
  public final void setFirstRowRelative(boolean rel) {
    field_3_first_column = rowRelative.setBoolean(field_3_first_column, rel);
  }

  /** @return isrelative first column to relative or not */
  public final boolean isFirstColRelative() {
    return colRelative.isSet(field_3_first_column);
  }

  /** set whether the first column is relative */
  public final void setFirstColRelative(boolean rel) {
    field_3_first_column = colRelative.setBoolean(field_3_first_column, rel);
  }

  /** set the first column in the area */
  public final void setFirstColumn(int colIx) {
    field_3_first_column = columnMask.setValue(field_3_first_column, colIx);
  }

  /** set the first column irrespective of the bitmasks */
  public final void setFirstColumnRaw(int column) {
    field_3_first_column = column;
  }

  /** @return lastcolumn in the area */
  public final int getLastColumn() {
    return columnMask.getValue(field_4_last_column);
  }

  /** @return last column and bitmask (the raw field) */
  public final short getLastColumnRaw() {
    return (short) field_4_last_column;
  }

  /** @return last row relative or not */
  public final boolean isLastRowRelative() {
    return rowRelative.isSet(field_4_last_column);
  }

  /**
   * set whether the last row is relative or not
   *
   * @param rel <code>true</code> if the last row relative, else <code>false</code>
   */
  public final void setLastRowRelative(boolean rel) {
    field_4_last_column = rowRelative.setBoolean(field_4_last_column, rel);
  }

  /** @return lastcol relative or not */
  public final boolean isLastColRelative() {
    return colRelative.isSet(field_4_last_column);
  }

  /** set whether the last column should be relative or not */
  public final void setLastColRelative(boolean rel) {
    field_4_last_column = colRelative.setBoolean(field_4_last_column, rel);
  }

  /** set the last column in the area */
  public final void setLastColumn(int colIx) {
    field_4_last_column = columnMask.setValue(field_4_last_column, colIx);
  }

  /** set the last column irrespective of the bitmasks */
  public final void setLastColumnRaw(short column) {
    field_4_last_column = column;
  }

  protected final String formatReferenceAsString() {
    CellReference topLeft =
        new CellReference(
            getFirstRow(), getFirstColumn(), !isFirstRowRelative(), !isFirstColRelative());
    CellReference botRight =
        new CellReference(
            getLastRow(), getLastColumn(), !isLastRowRelative(), !isLastColRelative());

    if (AreaReference.isWholeColumnReference(SpreadsheetVersion.EXCEL97, topLeft, botRight)) {
      return (new AreaReference(topLeft, botRight)).formatAsString();
    }
    return topLeft.formatAsString() + ":" + botRight.formatAsString();
  }

  public String toFormulaString() {
    return formatReferenceAsString();
  }

  public byte getDefaultOperandClass() {
    return Ptg.CLASS_REF;
  }
}
Example #8
0
/**
 * Title: Unicode String
 *
 * <p>Description: Unicode String - just standard fields that are in several records. It is
 * considered more desirable then repeating it in all of them.
 *
 * <p>This is often called a XLUnicodeRichExtendedString in MS documentation.
 *
 * <p>REFERENCE: PG 264 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2)
 *
 * <p>REFERENCE: PG 951 Excel Binary File Format (.xls) Structure Specification v20091214
 */
public class UnicodeString
    implements Comparable<
        UnicodeString> { // TODO - make this final when the compatibility version is removed
  private short field_1_charCount;
  private byte field_2_optionflags;
  private String field_3_string;
  private List<FormatRun> field_4_format_runs;
  private ExtRst field_5_ext_rst;
  private static final BitField highByte = BitFieldFactory.getInstance(0x1);
  // 0x2 is reserved
  private static final BitField extBit = BitFieldFactory.getInstance(0x4);
  private static final BitField richText = BitFieldFactory.getInstance(0x8);

  public static class FormatRun implements Comparable<FormatRun> {
    final short _character;
    short _fontIndex;

    public FormatRun(short character, short fontIndex) {
      this._character = character;
      this._fontIndex = fontIndex;
    }

    public FormatRun(LittleEndianInput in) {
      this(in.readShort(), in.readShort());
    }

    public short getCharacterPos() {
      return _character;
    }

    public short getFontIndex() {
      return _fontIndex;
    }

    public boolean equals(Object o) {
      if (!(o instanceof FormatRun)) {
        return false;
      }
      FormatRun other = (FormatRun) o;

      return _character == other._character && _fontIndex == other._fontIndex;
    }

    public int compareTo(FormatRun r) {
      if (_character == r._character && _fontIndex == r._fontIndex) {
        return 0;
      }
      if (_character == r._character) {
        return _fontIndex - r._fontIndex;
      }
      return _character - r._character;
    }

    public String toString() {
      return "character=" + _character + ",fontIndex=" + _fontIndex;
    }

    public void serialize(LittleEndianOutput out) {
      out.writeShort(_character);
      out.writeShort(_fontIndex);
    }
  }

  // See page 681
  public static class ExtRst implements Comparable<ExtRst> {
    private short reserved;

    // This is a Phs (see page 881)
    private short formattingFontIndex;
    private short formattingOptions;

    // This is a RPHSSub (see page 894)
    private int numberOfRuns;
    private String phoneticText;

    // This is an array of PhRuns (see page 881)
    private PhRun[] phRuns;
    // Sometimes there's some cruft at the end
    private byte[] extraData;

    private void populateEmpty() {
      reserved = 1;
      phoneticText = "";
      phRuns = new PhRun[0];
      extraData = new byte[0];
    }

    protected ExtRst() {
      populateEmpty();
    }

    protected ExtRst(LittleEndianInput in, int expectedLength) {
      reserved = in.readShort();

      // Old style detection (Reserved = 0xFF)
      if (reserved == -1) {
        populateEmpty();
        return;
      }

      // Spot corrupt records
      if (reserved != 1) {
        System.err.println(
            "Warning - ExtRst was has wrong magic marker, expecting 1 but found "
                + reserved
                + " - ignoring");
        // Grab all the remaining data, and ignore it
        for (int i = 0; i < expectedLength - 2; i++) {
          in.readByte();
        }
        // And make us be empty
        populateEmpty();
        return;
      }

      // Carry on reading in as normal
      short stringDataSize = in.readShort();

      formattingFontIndex = in.readShort();
      formattingOptions = in.readShort();

      // RPHSSub
      numberOfRuns = in.readUShort();
      short length1 = in.readShort();
      // No really. Someone clearly forgot to read
      //  the docs on their datastructure...
      short length2 = in.readShort();
      // And sometimes they write out garbage :(
      if (length1 == 0 && length2 > 0) {
        length2 = 0;
      }
      if (length1 != length2) {
        throw new IllegalStateException(
            "The two length fields of the Phonetic Text don't agree! "
                + length1
                + " vs "
                + length2);
      }
      phoneticText = StringUtil.readUnicodeLE(in, length1);

      int runData = stringDataSize - 4 - 6 - (2 * phoneticText.length());
      int numRuns = (runData / 6);
      phRuns = new PhRun[numRuns];
      for (int i = 0; i < phRuns.length; i++) {
        phRuns[i] = new PhRun(in);
      }

      int extraDataLength = runData - (numRuns * 6);
      if (extraDataLength < 0) {
        System.err.println("Warning - ExtRst overran by " + (0 - extraDataLength) + " bytes");
        extraDataLength = 0;
      }
      extraData = new byte[extraDataLength];
      for (int i = 0; i < extraData.length; i++) {
        extraData[i] = in.readByte();
      }
    }
    /** Returns our size, excluding our 4 byte header */
    protected int getDataSize() {
      return 4 + 6 + (2 * phoneticText.length()) + (6 * phRuns.length) + extraData.length;
    }

    protected void serialize(ContinuableRecordOutput out) {
      int dataSize = getDataSize();

      out.writeContinueIfRequired(8);
      out.writeShort(reserved);
      out.writeShort(dataSize);
      out.writeShort(formattingFontIndex);
      out.writeShort(formattingOptions);

      out.writeContinueIfRequired(6);
      out.writeShort(numberOfRuns);
      out.writeShort(phoneticText.length());
      out.writeShort(phoneticText.length());

      out.writeContinueIfRequired(phoneticText.length() * 2);
      StringUtil.putUnicodeLE(phoneticText, out);

      for (int i = 0; i < phRuns.length; i++) {
        phRuns[i].serialize(out);
      }

      out.write(extraData);
    }

    public boolean equals(Object obj) {
      if (!(obj instanceof ExtRst)) {
        return false;
      }
      ExtRst other = (ExtRst) obj;
      return (compareTo(other) == 0);
    }

    public int compareTo(ExtRst o) {
      int result;

      result = reserved - o.reserved;
      if (result != 0) return result;
      result = formattingFontIndex - o.formattingFontIndex;
      if (result != 0) return result;
      result = formattingOptions - o.formattingOptions;
      if (result != 0) return result;
      result = numberOfRuns - o.numberOfRuns;
      if (result != 0) return result;

      result = phoneticText.compareTo(o.phoneticText);
      if (result != 0) return result;

      result = phRuns.length - o.phRuns.length;
      if (result != 0) return result;
      for (int i = 0; i < phRuns.length; i++) {
        result =
            phRuns[i].phoneticTextFirstCharacterOffset
                - o.phRuns[i].phoneticTextFirstCharacterOffset;
        if (result != 0) return result;
        result = phRuns[i].realTextFirstCharacterOffset - o.phRuns[i].realTextFirstCharacterOffset;
        if (result != 0) return result;
        result = phRuns[i].realTextFirstCharacterOffset - o.phRuns[i].realTextLength;
        if (result != 0) return result;
      }

      result = extraData.length - o.extraData.length;
      if (result != 0) return result;

      // If we get here, it's the same
      return 0;
    }

    protected ExtRst clone() {
      ExtRst ext = new ExtRst();
      ext.reserved = reserved;
      ext.formattingFontIndex = formattingFontIndex;
      ext.formattingOptions = formattingOptions;
      ext.numberOfRuns = numberOfRuns;
      ext.phoneticText = new String(phoneticText);
      ext.phRuns = new PhRun[phRuns.length];
      for (int i = 0; i < ext.phRuns.length; i++) {
        ext.phRuns[i] =
            new PhRun(
                phRuns[i].phoneticTextFirstCharacterOffset,
                phRuns[i].realTextFirstCharacterOffset,
                phRuns[i].realTextLength);
      }
      return ext;
    }
  }

  public static class PhRun {
    private int phoneticTextFirstCharacterOffset;
    private int realTextFirstCharacterOffset;
    private int realTextLength;

    public PhRun(
        int phoneticTextFirstCharacterOffset,
        int realTextFirstCharacterOffset,
        int realTextLength) {
      this.phoneticTextFirstCharacterOffset = phoneticTextFirstCharacterOffset;
      this.realTextFirstCharacterOffset = realTextFirstCharacterOffset;
      this.realTextLength = realTextLength;
    }

    private PhRun(LittleEndianInput in) {
      phoneticTextFirstCharacterOffset = in.readUShort();
      realTextFirstCharacterOffset = in.readUShort();
      realTextLength = in.readUShort();
    }

    private void serialize(ContinuableRecordOutput out) {
      out.writeContinueIfRequired(6);
      out.writeShort(phoneticTextFirstCharacterOffset);
      out.writeShort(realTextFirstCharacterOffset);
      out.writeShort(realTextLength);
    }
  }

  private UnicodeString() {
    // Used for clone method.
  }

  public UnicodeString(String str) {
    setString(str);
  }

  public int hashCode() {
    int stringHash = 0;
    if (field_3_string != null) stringHash = field_3_string.hashCode();
    return field_1_charCount + stringHash;
  }

  /**
   * Our handling of equals is inconsistent with compareTo. The trouble is because we don't truely
   * understand rich text fields yet it's difficult to make a sound comparison.
   *
   * @param o The object to compare.
   * @return true if the object is actually equal.
   */
  public boolean equals(Object o) {
    if (!(o instanceof UnicodeString)) {
      return false;
    }
    UnicodeString other = (UnicodeString) o;

    // OK lets do this in stages to return a quickly, first check the actual string
    boolean eq =
        ((field_1_charCount == other.field_1_charCount)
            && (field_2_optionflags == other.field_2_optionflags)
            && field_3_string.equals(other.field_3_string));
    if (!eq) return false;

    // OK string appears to be equal but now lets compare formatting runs
    if ((field_4_format_runs == null) && (other.field_4_format_runs == null))
      // Strings are equal, and there are not formatting runs.
      return true;
    if (((field_4_format_runs == null) && (other.field_4_format_runs != null))
        || (field_4_format_runs != null) && (other.field_4_format_runs == null))
      // Strings are equal, but one or the other has formatting runs
      return false;

    // Strings are equal, so now compare formatting runs.
    int size = field_4_format_runs.size();
    if (size != other.field_4_format_runs.size()) return false;

    for (int i = 0; i < size; i++) {
      FormatRun run1 = field_4_format_runs.get(i);
      FormatRun run2 = other.field_4_format_runs.get(i);

      if (!run1.equals(run2)) return false;
    }

    // Well the format runs are equal as well!, better check the ExtRst data
    if (field_5_ext_rst == null && other.field_5_ext_rst == null) {
      // Good
    } else if (field_5_ext_rst != null && other.field_5_ext_rst != null) {
      int extCmp = field_5_ext_rst.compareTo(other.field_5_ext_rst);
      if (extCmp == 0) {
        // Good
      } else {
        return false;
      }
    } else {
      return false;
    }

    // Phew!! After all of that we have finally worked out that the strings
    // are identical.
    return true;
  }

  /**
   * construct a unicode string record and fill its fields, ID is ignored
   *
   * @param in the RecordInputstream to read the record from
   */
  public UnicodeString(RecordInputStream in) {
    field_1_charCount = in.readShort();
    field_2_optionflags = in.readByte();

    int runCount = 0;
    int extensionLength = 0;
    // Read the number of rich runs if rich text.
    if (isRichText()) {
      runCount = in.readShort();
    }
    // Read the size of extended data if present.
    if (isExtendedText()) {
      extensionLength = in.readInt();
    }

    boolean isCompressed = ((field_2_optionflags & 1) == 0);
    if (isCompressed) {
      field_3_string = in.readCompressedUnicode(getCharCount());
    } else {
      field_3_string = in.readUnicodeLEString(getCharCount());
    }

    if (isRichText() && (runCount > 0)) {
      field_4_format_runs = new ArrayList<FormatRun>(runCount);
      for (int i = 0; i < runCount; i++) {
        field_4_format_runs.add(new FormatRun(in));
      }
    }

    if (isExtendedText() && (extensionLength > 0)) {
      field_5_ext_rst = new ExtRst(in, extensionLength);
      if (field_5_ext_rst.getDataSize() + 4 != extensionLength) {
        System.err.println(
            "ExtRst was supposed to be "
                + extensionLength
                + " bytes long, but seems to actually be "
                + (field_5_ext_rst.getDataSize() + 4));
      }
    }
  }

  /**
   * get the number of characters in the string, as an un-wrapped int
   *
   * @return number of characters
   */
  public int getCharCount() {
    if (field_1_charCount < 0) {
      return field_1_charCount + 65536;
    }
    return field_1_charCount;
  }

  /**
   * set the number of characters in the string
   *
   * @param cc - number of characters
   */
  public void setCharCount(short cc) {
    field_1_charCount = cc;
  }

  /**
   * get the option flags which among other things return if this is a 16-bit or 8 bit string
   *
   * @return optionflags bitmask
   */
  public byte getOptionFlags() {
    return field_2_optionflags;
  }

  /** @return the actual string this contains as a java String object */
  public String getString() {
    return field_3_string;
  }

  /**
   * set the actual string this contains
   *
   * @param string the text
   */
  public void setString(String string) {
    field_3_string = string;
    setCharCount((short) field_3_string.length());
    // scan for characters greater than 255 ... if any are
    // present, we have to use 16-bit encoding. Otherwise, we
    // can use 8-bit encoding
    boolean useUTF16 = false;
    int strlen = string.length();

    for (int j = 0; j < strlen; j++) {
      if (string.charAt(j) > 255) {
        useUTF16 = true;
        break;
      }
    }
    if (useUTF16)
      // Set the uncompressed bit
      field_2_optionflags = highByte.setByte(field_2_optionflags);
    else field_2_optionflags = highByte.clearByte(field_2_optionflags);
  }

  public int getFormatRunCount() {
    if (field_4_format_runs == null) return 0;
    return field_4_format_runs.size();
  }

  public FormatRun getFormatRun(int index) {
    if (field_4_format_runs == null) {
      return null;
    }
    if (index < 0 || index >= field_4_format_runs.size()) {
      return null;
    }
    return field_4_format_runs.get(index);
  }

  private int findFormatRunAt(int characterPos) {
    int size = field_4_format_runs.size();
    for (int i = 0; i < size; i++) {
      FormatRun r = field_4_format_runs.get(i);
      if (r._character == characterPos) return i;
      else if (r._character > characterPos) return -1;
    }
    return -1;
  }

  /**
   * Adds a font run to the formatted string.
   *
   * <p>If a font run exists at the current charcter location, then it is replaced with the font run
   * to be added.
   */
  public void addFormatRun(FormatRun r) {
    if (field_4_format_runs == null) {
      field_4_format_runs = new ArrayList<FormatRun>();
    }

    int index = findFormatRunAt(r._character);
    if (index != -1) field_4_format_runs.remove(index);

    field_4_format_runs.add(r);
    // Need to sort the font runs to ensure that the font runs appear in
    // character order
    Collections.sort(field_4_format_runs);

    // Make sure that we now say that we are a rich string
    field_2_optionflags = richText.setByte(field_2_optionflags);
  }

  public Iterator<FormatRun> formatIterator() {
    if (field_4_format_runs != null) {
      return field_4_format_runs.iterator();
    }
    return null;
  }

  /**
   * unlike the real records we return the same as "getString()" rather than debug info
   *
   * @see #getDebugInfo()
   * @return String value of the record
   */
  public String toString() {
    return getString();
  }

  /**
   * return a character representation of the fields of this record
   *
   * @return String of output for biffviewer etc.
   */
  public String getDebugInfo() {
    StringBuffer buffer = new StringBuffer();

    buffer.append("[UNICODESTRING]\n");
    buffer
        .append("    .charcount       = ")
        .append(Integer.toHexString(getCharCount()))
        .append("\n");
    buffer
        .append("    .optionflags     = ")
        .append(Integer.toHexString(getOptionFlags()))
        .append("\n");
    buffer.append("    .string          = ").append(getString()).append("\n");
    if (field_4_format_runs != null) {
      for (int i = 0; i < field_4_format_runs.size(); i++) {
        FormatRun r = field_4_format_runs.get(i);
        buffer.append("      .format_run" + i + "          = ").append(r.toString()).append("\n");
      }
    }
    if (field_5_ext_rst != null) {
      buffer.append("    .field_5_ext_rst          = ").append("\n");
      buffer.append(field_5_ext_rst.toString()).append("\n");
    }
    buffer.append("[/UNICODESTRING]\n");
    return buffer.toString();
  }

  /**
   * Serialises out the String. There are special rules about where we can and can't split onto
   * Continue records.
   */
  public void serialize(ContinuableRecordOutput out) {
    int numberOfRichTextRuns = 0;
    int extendedDataSize = 0;
    if (isRichText() && field_4_format_runs != null) {
      numberOfRichTextRuns = field_4_format_runs.size();
    }
    if (isExtendedText() && field_5_ext_rst != null) {
      extendedDataSize = 4 + field_5_ext_rst.getDataSize();
    }

    // Serialise the bulk of the String
    // The writeString handles tricky continue stuff for us
    out.writeString(field_3_string, numberOfRichTextRuns, extendedDataSize);

    if (numberOfRichTextRuns > 0) {

      // This will ensure that a run does not split a continue
      for (int i = 0; i < numberOfRichTextRuns; i++) {
        if (out.getAvailableSpace() < 4) {
          out.writeContinue();
        }
        FormatRun r = field_4_format_runs.get(i);
        r.serialize(out);
      }
    }

    if (extendedDataSize > 0) {
      field_5_ext_rst.serialize(out);
    }
  }

  public int compareTo(UnicodeString str) {

    int result = getString().compareTo(str.getString());

    // As per the equals method lets do this in stages
    if (result != 0) return result;

    // OK string appears to be equal but now lets compare formatting runs
    if ((field_4_format_runs == null) && (str.field_4_format_runs == null))
      // Strings are equal, and there are no formatting runs.
      return 0;

    if ((field_4_format_runs == null) && (str.field_4_format_runs != null))
      // Strings are equal, but one or the other has formatting runs
      return 1;
    if ((field_4_format_runs != null) && (str.field_4_format_runs == null))
      // Strings are equal, but one or the other has formatting runs
      return -1;

    // Strings are equal, so now compare formatting runs.
    int size = field_4_format_runs.size();
    if (size != str.field_4_format_runs.size()) return size - str.field_4_format_runs.size();

    for (int i = 0; i < size; i++) {
      FormatRun run1 = field_4_format_runs.get(i);
      FormatRun run2 = str.field_4_format_runs.get(i);

      result = run1.compareTo(run2);
      if (result != 0) return result;
    }

    // Well the format runs are equal as well!, better check the ExtRst data
    if ((field_5_ext_rst == null) && (str.field_5_ext_rst == null)) return 0;
    if ((field_5_ext_rst == null) && (str.field_5_ext_rst != null)) return 1;
    if ((field_5_ext_rst != null) && (str.field_5_ext_rst == null)) return -1;

    result = field_5_ext_rst.compareTo(str.field_5_ext_rst);
    if (result != 0) return result;

    // Phew!! After all of that we have finally worked out that the strings
    // are identical.
    return 0;
  }

  private boolean isRichText() {
    return richText.isSet(getOptionFlags());
  }

  private boolean isExtendedText() {
    return extBit.isSet(getOptionFlags());
  }

  public Object clone() {
    UnicodeString str = new UnicodeString();
    str.field_1_charCount = field_1_charCount;
    str.field_2_optionflags = field_2_optionflags;
    str.field_3_string = field_3_string;
    if (field_4_format_runs != null) {
      str.field_4_format_runs = new ArrayList<FormatRun>();
      for (FormatRun r : field_4_format_runs) {
        str.field_4_format_runs.add(new FormatRun(r._character, r._fontIndex));
      }
    }
    if (field_5_ext_rst != null) {
      str.field_5_ext_rst = field_5_ext_rst.clone();
    }

    return str;
  }
}