001: /* ====================================================================
002: Licensed to the Apache Software Foundation (ASF) under one or more
003: contributor license agreements. See the NOTICE file distributed with
004: this work for additional information regarding copyright ownership.
005: The ASF licenses this file to You under the Apache License, Version 2.0
006: (the "License"); you may not use this file except in compliance with
007: the License. You may obtain a copy of the License at
008:
009: http://www.apache.org/licenses/LICENSE-2.0
010:
011: Unless required by applicable law or agreed to in writing, software
012: distributed under the License is distributed on an "AS IS" BASIS,
013: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: See the License for the specific language governing permissions and
015: limitations under the License.
016: ==================================================================== */
017:
018: package org.apache.poi.hssf.record;
019:
020: import org.apache.poi.util.LittleEndian;
021:
022: import java.io.IOException;
023: import java.io.InputStream;
024: import java.io.ByteArrayOutputStream;
025:
026: /**
027: * Title: Record Input Stream<P>
028: * Description: Wraps a stream and provides helper methods for the construction of records.<P>
029: *
030: * @author Jason Height (jheight @ apache dot org)
031: */
032:
033: public class RecordInputStream extends InputStream {
034: /** Maximum size of a single record (minus the 4 byte header) without a continue*/
035: public final static short MAX_RECORD_DATA_SIZE = 8224;
036:
037: private InputStream in;
038: protected short currentSid;
039: protected short currentLength = -1;
040: protected short nextSid = -1;
041:
042: protected byte[] data = new byte[MAX_RECORD_DATA_SIZE];
043: protected short recordOffset;
044: protected long pos;
045:
046: private boolean autoContinue = true;
047:
048: public RecordInputStream(InputStream in)
049: throws RecordFormatException {
050: this .in = in;
051: try {
052: nextSid = LittleEndian.readShort(in);
053: //Dont increment the pos just yet (technically we are at the start of
054: //the record stream until nextRecord is called).
055: } catch (IOException ex) {
056: throw new RecordFormatException("Error reading bytes", ex);
057: }
058: }
059:
060: /** This method will read a byte from the current record*/
061: public int read() throws IOException {
062: checkRecordPosition();
063:
064: byte result = data[recordOffset];
065: recordOffset += 1;
066: pos += 1;
067: return result;
068: }
069:
070: public short getSid() {
071: return currentSid;
072: }
073:
074: public short getLength() {
075: return currentLength;
076: }
077:
078: public short getRecordOffset() {
079: return recordOffset;
080: }
081:
082: public long getPos() {
083: return pos;
084: }
085:
086: public boolean hasNextRecord() {
087: return (nextSid != 0);
088: }
089:
090: /** Moves to the next record in the stream.
091: *
092: * <i>Note: The auto continue flag is reset to true</i>
093: */
094:
095: public void nextRecord() throws RecordFormatException {
096: if ((currentLength != -1) && (currentLength != recordOffset)) {
097: System.out.println("WARN. Unread " + remaining()
098: + " bytes of record 0x"
099: + Integer.toHexString(currentSid));
100: }
101: currentSid = nextSid;
102: pos += LittleEndian.SHORT_SIZE;
103: autoContinue = true;
104: try {
105: recordOffset = 0;
106: currentLength = LittleEndian.readShort(in);
107: if (currentLength > MAX_RECORD_DATA_SIZE)
108: throw new RecordFormatException(
109: "The content of an excel record cannot exceed "
110: + MAX_RECORD_DATA_SIZE + " bytes");
111: pos += LittleEndian.SHORT_SIZE;
112: in.read(data, 0, currentLength);
113:
114: //Read the Sid of the next record
115: nextSid = LittleEndian.readShort(in);
116: } catch (IOException ex) {
117: throw new RecordFormatException("Error reading bytes", ex);
118: }
119: }
120:
121: public void setAutoContinue(boolean enable) {
122: this .autoContinue = enable;
123: }
124:
125: public boolean getAutoContinue() {
126: return autoContinue;
127: }
128:
129: protected void checkRecordPosition() {
130: if (remaining() <= 0) {
131: if (isContinueNext() && autoContinue) {
132: nextRecord();
133: } else
134: throw new ArrayIndexOutOfBoundsException();
135: }
136: }
137:
138: public byte readByte() {
139: checkRecordPosition();
140:
141: byte result = data[recordOffset];
142: recordOffset += 1;
143: pos += 1;
144: return result;
145: }
146:
147: public short readShort() {
148: checkRecordPosition();
149:
150: short result = LittleEndian.getShort(data, recordOffset);
151: recordOffset += LittleEndian.SHORT_SIZE;
152: pos += LittleEndian.SHORT_SIZE;
153: return result;
154: }
155:
156: public int readInt() {
157: checkRecordPosition();
158:
159: int result = LittleEndian.getInt(data, recordOffset);
160: recordOffset += LittleEndian.INT_SIZE;
161: pos += LittleEndian.INT_SIZE;
162: return result;
163: }
164:
165: public long readLong() {
166: checkRecordPosition();
167:
168: long result = LittleEndian.getLong(data, recordOffset);
169: recordOffset += LittleEndian.LONG_SIZE;
170: pos += LittleEndian.LONG_SIZE;
171: return result;
172: }
173:
174: public int readUShort() {
175: checkRecordPosition();
176:
177: int result = LittleEndian.getUShort(data, recordOffset);
178: recordOffset += LittleEndian.SHORT_SIZE;
179: pos += LittleEndian.SHORT_SIZE;
180: return result;
181: }
182:
183: byte[] NAN_data = null;
184:
185: public double readDouble() {
186: checkRecordPosition();
187: //Reset NAN data
188: NAN_data = null;
189: double result = LittleEndian.getDouble(data, recordOffset);
190: //Excel represents NAN in several ways, at this point in time we do not often
191: //know the sequence of bytes, so as a hack we store the NAN byte sequence
192: //so that it is not corrupted.
193: if (Double.isNaN(result)) {
194: NAN_data = new byte[8];
195: System.arraycopy(data, recordOffset, NAN_data, 0, 8);
196: }
197:
198: recordOffset += LittleEndian.DOUBLE_SIZE;
199: pos += LittleEndian.DOUBLE_SIZE;
200: return result;
201: }
202:
203: public byte[] getNANData() {
204: if (NAN_data == null)
205: throw new RecordFormatException(
206: "Do NOT call getNANData without calling readDouble that returns NaN");
207: return NAN_data;
208: }
209:
210: public short[] readShortArray() {
211: checkRecordPosition();
212:
213: short[] arr = LittleEndian.getShortArray(data, recordOffset);
214: final int size = (2 * (arr.length + 1));
215: recordOffset += size;
216: pos += size;
217:
218: return arr;
219: }
220:
221: /**
222: * given a byte array of 16-bit unicode characters, compress to 8-bit and
223: * return a string
224: *
225: * { 0x16, 0x00 } -0x16
226: *
227: * @param length the length of the final string
228: * @return the converted string
229: * @exception IllegalArgumentException if len is too large (i.e.,
230: * there is not enough data in string to create a String of that
231: * length)
232: */
233: public String readUnicodeLEString(int length) {
234: if ((length < 0)
235: || (((remaining() / 2) < length) && !isContinueNext())) {
236: throw new IllegalArgumentException("Illegal length");
237: }
238:
239: StringBuffer buf = new StringBuffer(length);
240: for (int i = 0; i < length; i++) {
241: if ((remaining() == 0) && (isContinueNext())) {
242: nextRecord();
243: int compressByte = readByte();
244: if (compressByte != 1)
245: throw new IllegalArgumentException(
246: "compressByte in continue records must be 1 while reading unicode LE string");
247: }
248: char ch = (char) readShort();
249: buf.append(ch);
250: }
251: return buf.toString();
252: }
253:
254: public String readCompressedUnicode(int length) {
255: if ((length < 0)
256: || ((remaining() < length) && !isContinueNext())) {
257: throw new IllegalArgumentException("Illegal length");
258: }
259:
260: StringBuffer buf = new StringBuffer(length);
261: for (int i = 0; i < length; i++) {
262: if ((remaining() == 0) && (isContinueNext())) {
263: nextRecord();
264: int compressByte = readByte();
265: if (compressByte != 0)
266: throw new IllegalArgumentException(
267: "compressByte in continue records must be 0 while reading compressed unicode");
268: }
269: byte b = readByte();
270: //Typecast direct to char from byte with high bit set causes all ones
271: //in the high byte of the char (which is of course incorrect)
272: char ch = (char) ((short) 0xff & (short) b);
273: buf.append(ch);
274: }
275: return buf.toString();
276: }
277:
278: /** Returns an excel style unicode string from the bytes reminaing in the record.
279: * <i>Note:</i> Unicode strings differ from <b>normal</b> strings due to the addition of
280: * formatting information.
281: *
282: * @return The unicode string representation of the remaining bytes.
283: */
284: public UnicodeString readUnicodeString() {
285: return new UnicodeString(this );
286: }
287:
288: /** Returns the remaining bytes for the current record.
289: *
290: * @return The remaining bytes of the current record.
291: */
292: public byte[] readRemainder() {
293: int size = remaining();
294: byte[] result = new byte[size];
295: System.arraycopy(data, recordOffset, result, 0, size);
296: recordOffset += size;
297: pos += size;
298: return result;
299: }
300:
301: /** Reads all byte data for the current record, including any
302: * that overlaps into any following continue records.
303: *
304: * @deprecated Best to write a input stream that wraps this one where there is
305: * special sub record that may overlap continue records.
306: */
307: public byte[] readAllContinuedRemainder() {
308: //Using a ByteArrayOutputStream is just an easy way to get a
309: //growable array of the data.
310: ByteArrayOutputStream out = new ByteArrayOutputStream(
311: 2 * MAX_RECORD_DATA_SIZE);
312:
313: while (isContinueNext()) {
314: byte[] b = readRemainder();
315: out.write(b, 0, b.length);
316: nextRecord();
317: }
318: byte[] b = readRemainder();
319: out.write(b, 0, b.length);
320:
321: return out.toByteArray();
322: }
323:
324: /** The remaining number of bytes in the <i>current</i> record.
325: *
326: * @return The number of bytes remaining in the current record
327: */
328: public int remaining() {
329: return (currentLength - recordOffset);
330: }
331:
332: /** Returns true iif a Continue record is next in the excel stream
333: *
334: * @return True when a ContinueRecord is next.
335: */
336: public boolean isContinueNext() {
337: return (nextSid == ContinueRecord.sid);
338: }
339: }
|