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.hpsf;
019:
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.UnsupportedEncodingException;
023: import java.util.ArrayList;
024: import java.util.List;
025:
026: import org.apache.poi.hpsf.wellknown.SectionIDMap;
027: import org.apache.poi.util.LittleEndian;
028:
029: /**
030: * <p>Represents a property set in the Horrible Property Set Format
031: * (HPSF). These are usually metadata of a Microsoft Office
032: * document.</p>
033: *
034: * <p>An application that wants to access these metadata should create
035: * an instance of this class or one of its subclasses by calling the
036: * factory method {@link PropertySetFactory#create} and then retrieve
037: * the information its needs by calling appropriate methods.</p>
038: *
039: * <p>{@link PropertySetFactory#create} does its work by calling one
040: * of the constructors {@link PropertySet#PropertySet(InputStream)} or
041: * {@link PropertySet#PropertySet(byte[])}. If the constructor's
042: * argument is not in the Horrible Property Set Format, i.e. not a
043: * property set stream, or if any other error occurs, an appropriate
044: * exception is thrown.</p>
045: *
046: * <p>A {@link PropertySet} has a list of {@link Section}s, and each
047: * {@link Section} has a {@link Property} array. Use {@link
048: * #getSections} to retrieve the {@link Section}s, then call {@link
049: * Section#getProperties} for each {@link Section} to get hold of the
050: * {@link Property} arrays.</p> Since the vast majority of {@link
051: * PropertySet}s contains only a single {@link Section}, the
052: * convenience method {@link #getProperties} returns the properties of
053: * a {@link PropertySet}'s {@link Section} (throwing a {@link
054: * NoSingleSectionException} if the {@link PropertySet} contains more
055: * (or less) than exactly one {@link Section}).</p>
056: *
057: * @author Rainer Klute <a
058: * href="mailto:klute@rainer-klute.de"><klute@rainer-klute.de></a>
059: * @author Drew Varner (Drew.Varner hanginIn sc.edu)
060: * @version $Id: PropertySet.java 489730 2006-12-22 19:18:16Z bayard $
061: * @since 2002-02-09
062: */
063: public class PropertySet {
064:
065: /**
066: * <p>The "byteOrder" field must equal this value.</p>
067: */
068: static final byte[] BYTE_ORDER_ASSERTION = new byte[] {
069: (byte) 0xFE, (byte) 0xFF };
070:
071: /**
072: * <p>Specifies this {@link PropertySet}'s byte order. See the
073: * HPFS documentation for details!</p>
074: */
075: protected int byteOrder;
076:
077: /**
078: * <p>Returns the property set stream's low-level "byte order"
079: * field. It is always <tt>0xFFFE</tt> .</p>
080: *
081: * @return The property set stream's low-level "byte order" field.
082: */
083: public int getByteOrder() {
084: return byteOrder;
085: }
086:
087: /**
088: * <p>The "format" field must equal this value.</p>
089: */
090: static final byte[] FORMAT_ASSERTION = new byte[] { (byte) 0x00,
091: (byte) 0x00 };
092:
093: /**
094: * <p>Specifies this {@link PropertySet}'s format. See the HPFS
095: * documentation for details!</p>
096: */
097: protected int format;
098:
099: /**
100: * <p>Returns the property set stream's low-level "format"
101: * field. It is always <tt>0x0000</tt> .</p>
102: *
103: * @return The property set stream's low-level "format" field.
104: */
105: public int getFormat() {
106: return format;
107: }
108:
109: /**
110: * <p>Specifies the version of the operating system that created
111: * this {@link PropertySet}. See the HPFS documentation for
112: * details!</p>
113: */
114: protected int osVersion;
115:
116: /**
117: * <p>If the OS version field holds this value the property set stream was
118: * created on a 16-bit Windows system.</p>
119: */
120: public static final int OS_WIN16 = 0x0000;
121:
122: /**
123: * <p>If the OS version field holds this value the property set stream was
124: * created on a Macintosh system.</p>
125: */
126: public static final int OS_MACINTOSH = 0x0001;
127:
128: /**
129: * <p>If the OS version field holds this value the property set stream was
130: * created on a 32-bit Windows system.</p>
131: */
132: public static final int OS_WIN32 = 0x0002;
133:
134: /**
135: * <p>Returns the property set stream's low-level "OS version"
136: * field.</p>
137: *
138: * @return The property set stream's low-level "OS version" field.
139: */
140: public int getOSVersion() {
141: return osVersion;
142: }
143:
144: /**
145: * <p>Specifies this {@link PropertySet}'s "classID" field. See
146: * the HPFS documentation for details!</p>
147: */
148: protected ClassID classID;
149:
150: /**
151: * <p>Returns the property set stream's low-level "class ID"
152: * field.</p>
153: *
154: * @return The property set stream's low-level "class ID" field.
155: */
156: public ClassID getClassID() {
157: return classID;
158: }
159:
160: /**
161: * <p>Returns the number of {@link Section}s in the property
162: * set.</p>
163: *
164: * @return The number of {@link Section}s in the property set.
165: */
166: public int getSectionCount() {
167: return sections.size();
168: }
169:
170: /**
171: * <p>The sections in this {@link PropertySet}.</p>
172: */
173: protected List sections;
174:
175: /**
176: * <p>Returns the {@link Section}s in the property set.</p>
177: *
178: * @return The {@link Section}s in the property set.
179: */
180: public List getSections() {
181: return sections;
182: }
183:
184: /**
185: * <p>Creates an empty (uninitialized) {@link PropertySet}.</p>
186: *
187: * <p><strong>Please note:</strong> For the time being this
188: * constructor is protected since it is used for internal purposes
189: * only, but expect it to become public once the property set's
190: * writing functionality is implemented.</p>
191: */
192: protected PropertySet() {
193: }
194:
195: /**
196: * <p>Creates a {@link PropertySet} instance from an {@link
197: * InputStream} in the Horrible Property Set Format.</p>
198: *
199: * <p>The constructor reads the first few bytes from the stream
200: * and determines whether it is really a property set stream. If
201: * it is, it parses the rest of the stream. If it is not, it
202: * resets the stream to its beginning in order to let other
203: * components mess around with the data and throws an
204: * exception.</p>
205: *
206: * @param stream Holds the data making out the property set
207: * stream.
208: * @throws MarkUnsupportedException if the stream does not support
209: * the {@link InputStream#markSupported} method.
210: * @throws IOException if the {@link InputStream} cannot not be
211: * accessed as needed.
212: * @exception NoPropertySetStreamException if the input stream does not
213: * contain a property set.
214: * @exception UnsupportedEncodingException if a character encoding is not
215: * supported.
216: */
217: public PropertySet(final InputStream stream)
218: throws NoPropertySetStreamException,
219: MarkUnsupportedException, IOException,
220: UnsupportedEncodingException {
221: if (isPropertySetStream(stream)) {
222: final int avail = stream.available();
223: final byte[] buffer = new byte[avail];
224: stream.read(buffer, 0, buffer.length);
225: init(buffer, 0, buffer.length);
226: } else
227: throw new NoPropertySetStreamException();
228: }
229:
230: /**
231: * <p>Creates a {@link PropertySet} instance from a byte array
232: * that represents a stream in the Horrible Property Set
233: * Format.</p>
234: *
235: * @param stream The byte array holding the stream data.
236: * @param offset The offset in <var>stream</var> where the stream
237: * data begin. If the stream data begin with the first byte in the
238: * array, the <var>offset</var> is 0.
239: * @param length The length of the stream data.
240: * @throws NoPropertySetStreamException if the byte array is not a
241: * property set stream.
242: *
243: * @exception UnsupportedEncodingException if the codepage is not supported.
244: */
245: public PropertySet(final byte[] stream, final int offset,
246: final int length) throws NoPropertySetStreamException,
247: UnsupportedEncodingException {
248: if (isPropertySetStream(stream, offset, length))
249: init(stream, offset, length);
250: else
251: throw new NoPropertySetStreamException();
252: }
253:
254: /**
255: * <p>Creates a {@link PropertySet} instance from a byte array
256: * that represents a stream in the Horrible Property Set
257: * Format.</p>
258: *
259: * @param stream The byte array holding the stream data. The
260: * complete byte array contents is the stream data.
261: * @throws NoPropertySetStreamException if the byte array is not a
262: * property set stream.
263: *
264: * @exception UnsupportedEncodingException if the codepage is not supported.
265: */
266: public PropertySet(final byte[] stream)
267: throws NoPropertySetStreamException,
268: UnsupportedEncodingException {
269: this (stream, 0, stream.length);
270: }
271:
272: /**
273: * <p>Checks whether an {@link InputStream} is in the Horrible
274: * Property Set Format.</p>
275: *
276: * @param stream The {@link InputStream} to check. In order to
277: * perform the check, the method reads the first bytes from the
278: * stream. After reading, the stream is reset to the position it
279: * had before reading. The {@link InputStream} must support the
280: * {@link InputStream#mark} method.
281: * @return <code>true</code> if the stream is a property set
282: * stream, else <code>false</code>.
283: * @throws MarkUnsupportedException if the {@link InputStream}
284: * does not support the {@link InputStream#mark} method.
285: * @exception IOException if an I/O error occurs
286: */
287: public static boolean isPropertySetStream(final InputStream stream)
288: throws MarkUnsupportedException, IOException {
289: /*
290: * Read at most this many bytes.
291: */
292: final int BUFFER_SIZE = 50;
293:
294: /*
295: * Mark the current position in the stream so that we can
296: * reset to this position if the stream does not contain a
297: * property set.
298: */
299: if (!stream.markSupported())
300: throw new MarkUnsupportedException(stream.getClass()
301: .getName());
302: stream.mark(BUFFER_SIZE);
303:
304: /*
305: * Read a couple of bytes from the stream.
306: */
307: final byte[] buffer = new byte[BUFFER_SIZE];
308: final int bytes = stream.read(buffer, 0, Math.min(
309: buffer.length, stream.available()));
310: final boolean isPropertySetStream = isPropertySetStream(buffer,
311: 0, bytes);
312: stream.reset();
313: return isPropertySetStream;
314: }
315:
316: /**
317: * <p>Checks whether a byte array is in the Horrible Property Set
318: * Format.</p>
319: *
320: * @param src The byte array to check.
321: * @param offset The offset in the byte array.
322: * @param length The significant number of bytes in the byte
323: * array. Only this number of bytes will be checked.
324: * @return <code>true</code> if the byte array is a property set
325: * stream, <code>false</code> if not.
326: */
327: public static boolean isPropertySetStream(final byte[] src,
328: final int offset, final int length) {
329: /* FIXME (3): Ensure that at most "length" bytes are read. */
330:
331: /*
332: * Read the header fields of the stream. They must always be
333: * there.
334: */
335: int o = offset;
336: final int byteOrder = LittleEndian.getUShort(src, o);
337: o += LittleEndian.SHORT_SIZE;
338: byte[] temp = new byte[LittleEndian.SHORT_SIZE];
339: LittleEndian.putShort(temp, (short) byteOrder);
340: if (!Util.equal(temp, BYTE_ORDER_ASSERTION))
341: return false;
342: final int format = LittleEndian.getUShort(src, o);
343: o += LittleEndian.SHORT_SIZE;
344: temp = new byte[LittleEndian.SHORT_SIZE];
345: LittleEndian.putShort(temp, (short) format);
346: if (!Util.equal(temp, FORMAT_ASSERTION))
347: return false;
348: // final long osVersion = LittleEndian.getUInt(src, offset);
349: o += LittleEndian.INT_SIZE;
350: // final ClassID classID = new ClassID(src, offset);
351: o += ClassID.LENGTH;
352: final long sectionCount = LittleEndian.getUInt(src, o);
353: o += LittleEndian.INT_SIZE;
354: if (sectionCount < 1)
355: return false;
356: return true;
357: }
358:
359: /**
360: * <p>Initializes this {@link PropertySet} instance from a byte
361: * array. The method assumes that it has been checked already that
362: * the byte array indeed represents a property set stream. It does
363: * no more checks on its own.</p>
364: *
365: * @param src Byte array containing the property set stream
366: * @param offset The property set stream starts at this offset
367: * from the beginning of <var>src</var>
368: * @param length Length of the property set stream.
369: * @throws UnsupportedEncodingException if HPSF does not (yet) support the
370: * property set's character encoding.
371: */
372: private void init(final byte[] src, final int offset,
373: final int length) throws UnsupportedEncodingException {
374: /* FIXME (3): Ensure that at most "length" bytes are read. */
375:
376: /*
377: * Read the stream's header fields.
378: */
379: int o = offset;
380: byteOrder = LittleEndian.getUShort(src, o);
381: o += LittleEndian.SHORT_SIZE;
382: format = LittleEndian.getUShort(src, o);
383: o += LittleEndian.SHORT_SIZE;
384: osVersion = (int) LittleEndian.getUInt(src, o);
385: o += LittleEndian.INT_SIZE;
386: classID = new ClassID(src, o);
387: o += ClassID.LENGTH;
388: final int sectionCount = LittleEndian.getInt(src, o);
389: o += LittleEndian.INT_SIZE;
390: if (sectionCount <= 0)
391: throw new HPSFRuntimeException("Section count "
392: + sectionCount + " must be greater than 0.");
393:
394: /*
395: * Read the sections, which are following the header. They
396: * start with an array of section descriptions. Each one
397: * consists of a format ID telling what the section contains
398: * and an offset telling how many bytes from the start of the
399: * stream the section begins.
400: */
401: /*
402: * Most property sets have only one section. The Document
403: * Summary Information stream has 2. Everything else is a rare
404: * exception and is no longer fostered by Microsoft.
405: */
406: sections = new ArrayList(sectionCount);
407:
408: /*
409: * Loop over the section descriptor array. Each descriptor
410: * consists of a ClassID and a DWord, and we have to increment
411: * "offset" accordingly.
412: */
413: for (int i = 0; i < sectionCount; i++) {
414: final Section s = new Section(src, o);
415: o += ClassID.LENGTH + LittleEndian.INT_SIZE;
416: sections.add(s);
417: }
418: }
419:
420: /**
421: * <p>Checks whether this {@link PropertySet} represents a Summary
422: * Information.</p>
423: *
424: * @return <code>true</code> if this {@link PropertySet}
425: * represents a Summary Information, else <code>false</code>.
426: */
427: public boolean isSummaryInformation() {
428: return Util.equal(((Section) sections.get(0)).getFormatID()
429: .getBytes(), SectionIDMap.SUMMARY_INFORMATION_ID);
430: }
431:
432: /**
433: * <p>Checks whether this {@link PropertySet} is a Document
434: * Summary Information.</p>
435: *
436: * @return <code>true</code> if this {@link PropertySet}
437: * represents a Document Summary Information, else <code>false</code>.
438: */
439: public boolean isDocumentSummaryInformation() {
440: return Util.equal(((Section) sections.get(0)).getFormatID()
441: .getBytes(),
442: SectionIDMap.DOCUMENT_SUMMARY_INFORMATION_ID[0]);
443: }
444:
445: /**
446: * <p>Convenience method returning the {@link Property} array
447: * contained in this property set. It is a shortcut for getting
448: * the {@link PropertySet}'s {@link Section}s list and then
449: * getting the {@link Property} array from the first {@link
450: * Section}.</p>
451: *
452: * @return The properties of the only {@link Section} of this
453: * {@link PropertySet}.
454: * @throws NoSingleSectionException if the {@link PropertySet} has
455: * more or less than one {@link Section}.
456: */
457: public Property[] getProperties() throws NoSingleSectionException {
458: return getFirstSection().getProperties();
459: }
460:
461: /**
462: * <p>Convenience method returning the value of the property with
463: * the specified ID. If the property is not available,
464: * <code>null</code> is returned and a subsequent call to {@link
465: * #wasNull} will return <code>true</code> .</p>
466: *
467: * @param id The property ID
468: * @return The property value
469: * @throws NoSingleSectionException if the {@link PropertySet} has
470: * more or less than one {@link Section}.
471: */
472: protected Object getProperty(final int id)
473: throws NoSingleSectionException {
474: return getFirstSection().getProperty(id);
475: }
476:
477: /**
478: * <p>Convenience method returning the value of a boolean property
479: * with the specified ID. If the property is not available,
480: * <code>false</code> is returned. A subsequent call to {@link
481: * #wasNull} will return <code>true</code> to let the caller
482: * distinguish that case from a real property value of
483: * <code>false</code>.</p>
484: *
485: * @param id The property ID
486: * @return The property value
487: * @throws NoSingleSectionException if the {@link PropertySet} has
488: * more or less than one {@link Section}.
489: */
490: protected boolean getPropertyBooleanValue(final int id)
491: throws NoSingleSectionException {
492: return getFirstSection().getPropertyBooleanValue(id);
493: }
494:
495: /**
496: * <p>Convenience method returning the value of the numeric
497: * property with the specified ID. If the property is not
498: * available, 0 is returned. A subsequent call to {@link #wasNull}
499: * will return <code>true</code> to let the caller distinguish
500: * that case from a real property value of 0.</p>
501: *
502: * @param id The property ID
503: * @return The propertyIntValue value
504: * @throws NoSingleSectionException if the {@link PropertySet} has
505: * more or less than one {@link Section}.
506: */
507: protected int getPropertyIntValue(final int id)
508: throws NoSingleSectionException {
509: return getFirstSection().getPropertyIntValue(id);
510: }
511:
512: /**
513: * <p>Checks whether the property which the last call to {@link
514: * #getPropertyIntValue} or {@link #getProperty} tried to access
515: * was available or not. This information might be important for
516: * callers of {@link #getPropertyIntValue} since the latter
517: * returns 0 if the property does not exist. Using {@link
518: * #wasNull}, the caller can distiguish this case from a
519: * property's real value of 0.</p>
520: *
521: * @return <code>true</code> if the last call to {@link
522: * #getPropertyIntValue} or {@link #getProperty} tried to access a
523: * property that was not available, else <code>false</code>.
524: * @throws NoSingleSectionException if the {@link PropertySet} has
525: * more than one {@link Section}.
526: */
527: public boolean wasNull() throws NoSingleSectionException {
528: return getFirstSection().wasNull();
529: }
530:
531: /**
532: * <p>Gets the {@link PropertySet}'s first section.</p>
533: *
534: * @return The {@link PropertySet}'s first section.
535: */
536: public Section getFirstSection() {
537: if (getSectionCount() < 1)
538: throw new MissingSectionException(
539: "Property set does not contain any sections.");
540: return ((Section) sections.get(0));
541: }
542:
543: /**
544: * <p>If the {@link PropertySet} has only a single section this
545: * method returns it.</p>
546: *
547: * @return The singleSection value
548: */
549: public Section getSingleSection() {
550: final int sectionCount = getSectionCount();
551: if (sectionCount != 1)
552: throw new NoSingleSectionException("Property set contains "
553: + sectionCount + " sections.");
554: return ((Section) sections.get(0));
555: }
556:
557: /**
558: * <p>Returns <code>true</code> if the <code>PropertySet</code> is equal
559: * to the specified parameter, else <code>false</code>.</p>
560: *
561: * @param o the object to compare this <code>PropertySet</code> with
562: *
563: * @return <code>true</code> if the objects are equal, <code>false</code>
564: * if not
565: */
566: public boolean equals(final Object o) {
567: if (o == null || !(o instanceof PropertySet))
568: return false;
569: final PropertySet ps = (PropertySet) o;
570: int byteOrder1 = ps.getByteOrder();
571: int byteOrder2 = getByteOrder();
572: ClassID classID1 = ps.getClassID();
573: ClassID classID2 = getClassID();
574: int format1 = ps.getFormat();
575: int format2 = getFormat();
576: int osVersion1 = ps.getOSVersion();
577: int osVersion2 = getOSVersion();
578: int sectionCount1 = ps.getSectionCount();
579: int sectionCount2 = getSectionCount();
580: if (byteOrder1 != byteOrder2 || !classID1.equals(classID2)
581: || format1 != format2 || osVersion1 != osVersion2
582: || sectionCount1 != sectionCount2)
583: return false;
584:
585: /* Compare the sections: */
586: return Util.equals(getSections(), ps.getSections());
587: }
588:
589: /**
590: * @see Object#hashCode()
591: */
592: public int hashCode() {
593: throw new UnsupportedOperationException(
594: "FIXME: Not yet implemented.");
595: }
596:
597: /**
598: * @see Object#toString()
599: */
600: public String toString() {
601: final StringBuffer b = new StringBuffer();
602: final int sectionCount = getSectionCount();
603: b.append(getClass().getName());
604: b.append('[');
605: b.append("byteOrder: ");
606: b.append(getByteOrder());
607: b.append(", classID: ");
608: b.append(getClassID());
609: b.append(", format: ");
610: b.append(getFormat());
611: b.append(", OSVersion: ");
612: b.append(getOSVersion());
613: b.append(", sectionCount: ");
614: b.append(sectionCount);
615: b.append(", sections: [\n");
616: final List sections = getSections();
617: for (int i = 0; i < sectionCount; i++)
618: b.append(((Section) sections.get(i)).toString());
619: b.append(']');
620: b.append(']');
621: return b.toString();
622: }
623: }
|