You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PropertySet.java 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.hpsf;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.io.OutputStream;
  19. import java.io.UnsupportedEncodingException;
  20. import java.util.ArrayList;
  21. import java.util.Collections;
  22. import java.util.List;
  23. import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
  24. import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
  25. import org.apache.poi.EmptyFileException;
  26. import org.apache.poi.hpsf.wellknown.PropertyIDMap;
  27. import org.apache.poi.poifs.filesystem.DirectoryEntry;
  28. import org.apache.poi.poifs.filesystem.Entry;
  29. import org.apache.poi.util.CodePageUtil;
  30. import org.apache.poi.util.IOUtils;
  31. import org.apache.poi.util.LittleEndian;
  32. import org.apache.poi.util.LittleEndianByteArrayInputStream;
  33. import org.apache.poi.util.LittleEndianConsts;
  34. import org.apache.poi.util.LittleEndianOutputStream;
  35. import org.apache.poi.util.NotImplemented;
  36. /**
  37. * Represents a property set in the Horrible Property Set Format
  38. * (HPSF). These are usually metadata of a Microsoft Office
  39. * document.<p>
  40. *
  41. * An application that wants to access these metadata should create
  42. * an instance of this class or one of its subclasses by calling the
  43. * factory method {@link PropertySetFactory#create} and then retrieve
  44. * the information its needs by calling appropriate methods.<p>
  45. *
  46. * {@link PropertySetFactory#create} does its work by calling one
  47. * of the constructors {@link PropertySet#PropertySet(InputStream)} or
  48. * {@link PropertySet#PropertySet(byte[])}. If the constructor's
  49. * argument is not in the Horrible Property Set Format, i.e. not a
  50. * property set stream, or if any other error occurs, an appropriate
  51. * exception is thrown.<p>
  52. *
  53. * A {@link PropertySet} has a list of {@link Section}s, and each
  54. * {@link Section} has a {@link Property} array. Use {@link
  55. * #getSections} to retrieve the {@link Section}s, then call {@link
  56. * Section#getProperties} for each {@link Section} to get hold of the
  57. * {@link Property} arrays.<p>
  58. *
  59. * Since the vast majority of {@link PropertySet}s contains only a single
  60. * {@link Section}, the convenience method {@link #getProperties} returns
  61. * the properties of a {@link PropertySet}'s {@link Section} (throwing a
  62. * {@link NoSingleSectionException} if the {@link PropertySet} contains
  63. * more (or less) than exactly one {@link Section}).
  64. */
  65. public class PropertySet {
  66. /**
  67. * If the OS version field holds this value the property set stream was
  68. * created on a 16-bit Windows system.
  69. */
  70. public static final int OS_WIN16 = 0x0000;
  71. /**
  72. * If the OS version field holds this value the property set stream was
  73. * created on a Macintosh system.
  74. */
  75. public static final int OS_MACINTOSH = 0x0001;
  76. /**
  77. * If the OS version field holds this value the property set stream was
  78. * created on a 32-bit Windows system.
  79. */
  80. public static final int OS_WIN32 = 0x0002;
  81. /**
  82. * The "byteOrder" field must equal this value.
  83. */
  84. /* package */ static final int BYTE_ORDER_ASSERTION = 0xFFFE;
  85. /**
  86. * The "format" field must equal this value.
  87. */
  88. /* package */ static final int FORMAT_ASSERTION = 0x0000;
  89. /**
  90. * The length of the property set stream header.
  91. */
  92. /* package */ static final int OFFSET_HEADER =
  93. LittleEndianConsts.SHORT_SIZE + /* Byte order */
  94. LittleEndianConsts.SHORT_SIZE + /* Format */
  95. LittleEndianConsts.INT_SIZE + /* OS version */
  96. ClassID.LENGTH + /* Class ID */
  97. LittleEndianConsts.INT_SIZE; /* Section count */
  98. /**
  99. * Specifies this {@link PropertySet}'s byte order. See the
  100. * HPFS documentation for details!
  101. */
  102. private int byteOrder;
  103. /**
  104. * Specifies this {@link PropertySet}'s format. See the HPFS
  105. * documentation for details!
  106. */
  107. private int format;
  108. /**
  109. * Specifies the version of the operating system that created this
  110. * {@link PropertySet}. See the HPFS documentation for details!
  111. */
  112. private int osVersion;
  113. /**
  114. * Specifies this {@link PropertySet}'s "classID" field. See
  115. * the HPFS documentation for details!
  116. */
  117. private ClassID classID;
  118. /**
  119. * The sections in this {@link PropertySet}.
  120. */
  121. private final List<Section> sections = new ArrayList<>();
  122. /**
  123. * Constructs a {@code PropertySet} instance. Its
  124. * primary task is to initialize the field with their proper values.
  125. * It also sets fields that might change to reasonable defaults.
  126. */
  127. public PropertySet() {
  128. /* Initialize the "byteOrder" field. */
  129. byteOrder = BYTE_ORDER_ASSERTION;
  130. /* Initialize the "format" field. */
  131. format = FORMAT_ASSERTION;
  132. /* Initialize "osVersion" field as if the property has been created on
  133. * a Win32 platform, whether this is the case or not. */
  134. osVersion = (OS_WIN32 << 16) | 0x0A04;
  135. /* Initialize the "classID" field. */
  136. classID = new ClassID();
  137. /* Initialize the sections. Since property set must have at least
  138. * one section it is added right here. */
  139. addSection(new Section());
  140. }
  141. /**
  142. * Creates a PropertySet instance from an {@link
  143. * InputStream} in the Horrible Property Set Format.<p>
  144. *
  145. * The constructor reads the first few bytes from the stream
  146. * and determines whether it is really a property set stream. If
  147. * it is, it parses the rest of the stream. If it is not, it
  148. * resets the stream to its beginning in order to let other
  149. * components mess around with the data and throws an
  150. * exception.
  151. *
  152. * @param stream Holds the data making out the property set
  153. * stream.
  154. * @throws IOException
  155. * if the {@link InputStream} cannot be accessed as needed.
  156. * @throws NoPropertySetStreamException
  157. * if the input stream does not contain a property set.
  158. * @throws UnsupportedEncodingException
  159. * if a character encoding is not supported.
  160. */
  161. public PropertySet(final InputStream stream)
  162. throws NoPropertySetStreamException, IOException {
  163. if (!isPropertySetStream(stream)) {
  164. throw new NoPropertySetStreamException();
  165. }
  166. final byte[] buffer = IOUtils.toByteArray(stream);
  167. init(buffer, 0, buffer.length);
  168. }
  169. /**
  170. * Creates a PropertySet instance from a byte array that
  171. * represents a stream in the Horrible Property Set Format.
  172. *
  173. * @param stream The byte array holding the stream data.
  174. * @param offset The offset in {@code stream} where the stream
  175. * data begin. If the stream data begin with the first byte in the
  176. * array, the {@code offset} is 0.
  177. * @param length The length of the stream data.
  178. * @throws NoPropertySetStreamException if the byte array is not a
  179. * property set stream.
  180. *
  181. * @throws UnsupportedEncodingException if the codepage is not supported.
  182. */
  183. public PropertySet(final byte[] stream, final int offset, final int length)
  184. throws NoPropertySetStreamException, UnsupportedEncodingException {
  185. if (!isPropertySetStream(stream, offset, length)) {
  186. throw new NoPropertySetStreamException();
  187. }
  188. init(stream, offset, length);
  189. }
  190. /**
  191. * Creates a PropertySet instance from a byte array
  192. * that represents a stream in the Horrible Property Set Format.
  193. *
  194. * @param stream The byte array holding the stream data. The
  195. * complete byte array contents is the stream data.
  196. * @throws NoPropertySetStreamException if the byte array is not a
  197. * property set stream.
  198. *
  199. * @throws UnsupportedEncodingException if the codepage is not supported.
  200. */
  201. public PropertySet(final byte[] stream)
  202. throws NoPropertySetStreamException, UnsupportedEncodingException {
  203. this(stream, 0, stream.length);
  204. }
  205. /**
  206. * Constructs a {@code PropertySet} by doing a deep copy of
  207. * an existing {@code PropertySet}. All nested elements, i.e.
  208. * {@code Section}s and {@code Property} instances, will be their
  209. * counterparts in the new {@code PropertySet}.
  210. *
  211. * @param ps The property set to copy
  212. */
  213. public PropertySet(PropertySet ps) {
  214. setByteOrder(ps.getByteOrder());
  215. setFormat(ps.getFormat());
  216. setOSVersion(ps.getOSVersion());
  217. setClassID(ps.getClassID());
  218. for (final Section section : ps.getSections()) {
  219. sections.add(new Section(section));
  220. }
  221. }
  222. /**
  223. * @return The property set stream's low-level "byte order" field. It is always {@code 0xFFFE}.
  224. */
  225. public int getByteOrder() {
  226. return byteOrder;
  227. }
  228. /**
  229. * Returns the property set stream's low-level "byte order" field.
  230. *
  231. * @param byteOrder The property set stream's low-level "byte order" field.
  232. */
  233. @SuppressWarnings("WeakerAccess")
  234. public void setByteOrder(int byteOrder) {
  235. this.byteOrder = byteOrder;
  236. }
  237. /**
  238. * @return The property set stream's low-level "format" field. It is always {@code 0x0000}.
  239. */
  240. public int getFormat() {
  241. return format;
  242. }
  243. /**
  244. * Sets the property set stream's low-level "format" field.
  245. *
  246. * @param format The property set stream's low-level "format" field.
  247. */
  248. public void setFormat(int format) {
  249. this.format = format;
  250. }
  251. /**
  252. * @return The property set stream's low-level "OS version" field.
  253. */
  254. public int getOSVersion() {
  255. return osVersion;
  256. }
  257. /**
  258. * Sets the property set stream's low-level "OS version" field.
  259. *
  260. * @param osVersion The property set stream's low-level "OS version" field.
  261. */
  262. @SuppressWarnings("WeakerAccess")
  263. public void setOSVersion(int osVersion) {
  264. this.osVersion = osVersion;
  265. }
  266. /**
  267. * @return The property set stream's low-level "class ID" field.
  268. */
  269. public ClassID getClassID() {
  270. return classID;
  271. }
  272. /**
  273. * Sets the property set stream's low-level "class ID" field.
  274. *
  275. * @param classID The property set stream's low-level "class ID" field.
  276. */
  277. @SuppressWarnings("WeakerAccess")
  278. public void setClassID(ClassID classID) {
  279. this.classID = classID;
  280. }
  281. /**
  282. * @return The number of {@link Section}s in the property set.
  283. */
  284. public int getSectionCount() {
  285. return sections.size();
  286. }
  287. /**
  288. * @return The unmodifiable list of {@link Section}s in the property set.
  289. */
  290. public List<Section> getSections() {
  291. return Collections.unmodifiableList(sections);
  292. }
  293. /**
  294. * Adds a section to this property set.
  295. *
  296. * @param section The {@link Section} to add. It will be appended
  297. * after any sections that are already present in the property set
  298. * and thus become the last section.
  299. */
  300. public void addSection(final Section section) {
  301. sections.add(section);
  302. }
  303. /**
  304. * Removes all sections from this property set.
  305. */
  306. public void clearSections() {
  307. sections.clear();
  308. }
  309. /**
  310. * The id to name mapping of the properties in this set.
  311. *
  312. * @return the id to name mapping of the properties in this set or {@code null} if not applicable
  313. */
  314. public PropertyIDMap getPropertySetIDMap() {
  315. return null;
  316. }
  317. /**
  318. * Checks whether an {@link InputStream} is in the Horrible
  319. * Property Set Format.
  320. *
  321. * @param stream The {@link InputStream} to check. In order to
  322. * perform the check, the method reads the first bytes from the
  323. * stream. After reading, the stream is reset to the position it
  324. * had before reading. The {@link InputStream} must support the
  325. * {@link InputStream#mark} method.
  326. * @return {@code true} if the stream is a property set
  327. * stream, else {@code false}.
  328. * @throws IOException if an I/O error occurs
  329. */
  330. public static boolean isPropertySetStream(final InputStream stream) throws IOException {
  331. /*
  332. * Read at most this many bytes.
  333. */
  334. final int BUFFER_SIZE = 50;
  335. /*
  336. * Read a couple of bytes from the stream.
  337. */
  338. try {
  339. final byte[] buffer = IOUtils.peekFirstNBytes(stream, BUFFER_SIZE);
  340. return isPropertySetStream(buffer, 0, buffer.length);
  341. } catch (EmptyFileException e) {
  342. return false;
  343. }
  344. }
  345. /**
  346. * Checks whether a byte array is in the Horrible Property Set Format.
  347. *
  348. * @param src The byte array to check.
  349. * @param offset The offset in the byte array.
  350. * @param length The significant number of bytes in the byte
  351. * array. Only this number of bytes will be checked.
  352. * @return {@code true} if the byte array is a property set
  353. * stream, {@code false} if not.
  354. */
  355. @SuppressWarnings({"unused", "WeakerAccess"})
  356. public static boolean isPropertySetStream(final byte[] src, final int offset, final int length) {
  357. LittleEndianByteArrayInputStream leis = new LittleEndianByteArrayInputStream(src, offset, length);
  358. /*
  359. * Read the header fields of the stream. They must always be
  360. * there.
  361. */
  362. try {
  363. final int byteOrder = leis.readUShort();
  364. if (byteOrder != BYTE_ORDER_ASSERTION) {
  365. return false;
  366. }
  367. final int format = leis.readUShort();
  368. if (format != FORMAT_ASSERTION) {
  369. return false;
  370. }
  371. final long osVersion = leis.readUInt();
  372. if (leis.skip(ClassID.LENGTH) != ClassID.LENGTH) {
  373. return false;
  374. }
  375. final long sectionCount = leis.readUInt();
  376. return (sectionCount >= 0);
  377. } catch (RuntimeException e) {
  378. return false;
  379. }
  380. }
  381. /**
  382. * Initializes this PropertySet instance from a byte
  383. * array. The method assumes that it has been checked already that
  384. * the byte array indeed represents a property set stream. It does
  385. * no more checks on its own.
  386. *
  387. * @param src Byte array containing the property set stream
  388. * @param offset The property set stream starts at this offset
  389. * from the beginning of {@code src}
  390. * @param length Length of the property set stream.
  391. * @throws UnsupportedEncodingException if HPSF does not (yet) support the
  392. * property set's character encoding.
  393. */
  394. private void init(final byte[] src, final int offset, final int length)
  395. throws UnsupportedEncodingException {
  396. /* FIXME (3): Ensure that at most "length" bytes are read. */
  397. /*
  398. * Read the stream's header fields.
  399. */
  400. int o = offset;
  401. byteOrder = LittleEndian.getUShort(src, o);
  402. o += LittleEndianConsts.SHORT_SIZE;
  403. format = LittleEndian.getUShort(src, o);
  404. o += LittleEndianConsts.SHORT_SIZE;
  405. osVersion = (int) LittleEndian.getUInt(src, o);
  406. o += LittleEndianConsts.INT_SIZE;
  407. classID = new ClassID(src, o);
  408. o += ClassID.LENGTH;
  409. final int sectionCount = LittleEndian.getInt(src, o);
  410. o += LittleEndianConsts.INT_SIZE;
  411. if (sectionCount < 0) {
  412. throw new HPSFRuntimeException("Section count " + sectionCount + " is negative.");
  413. }
  414. /*
  415. * Read the sections, which are following the header. They
  416. * start with an array of section descriptions. Each one
  417. * consists of a format ID telling what the section contains
  418. * and an offset telling how many bytes from the start of the
  419. * stream the section begins.
  420. *
  421. * Most property sets have only one section. The Document
  422. * Summary Information stream has 2. Everything else is a rare
  423. * exception and is no longer fostered by Microsoft.
  424. */
  425. /*
  426. * Loop over the section descriptor array. Each descriptor
  427. * consists of a ClassID and a DWord, and we have to increment
  428. * "offset" accordingly.
  429. */
  430. for (int i = 0; i < sectionCount; i++) {
  431. final Section s = new Section(src, o);
  432. o += ClassID.LENGTH + LittleEndianConsts.INT_SIZE;
  433. sections.add(s);
  434. }
  435. }
  436. /**
  437. * Writes the property set to an output stream.
  438. *
  439. * @param out the output stream to write the section to
  440. * @throws IOException if an error when writing to the output stream
  441. * occurs
  442. * @throws WritingNotSupportedException if HPSF does not yet support
  443. * writing a property's variant type.
  444. */
  445. public void write(final OutputStream out) throws IOException, WritingNotSupportedException {
  446. out.write(toBytes());
  447. /* Indicate that we're done */
  448. out.close();
  449. }
  450. private byte[] toBytes() throws WritingNotSupportedException, IOException {
  451. try (UnsynchronizedByteArrayOutputStream bos = new UnsynchronizedByteArrayOutputStream();
  452. LittleEndianOutputStream leos = new LittleEndianOutputStream(bos)) {
  453. /* Write the number of sections in this property set stream. */
  454. final int nrSections = getSectionCount();
  455. /* Write the property set's header. */
  456. leos.writeShort(getByteOrder());
  457. leos.writeShort(getFormat());
  458. leos.writeInt(getOSVersion());
  459. putClassId(bos, getClassID());
  460. leos.writeInt(nrSections);
  461. assert (bos.size() == OFFSET_HEADER);
  462. final int[][] offsets = new int[getSectionCount()][2];
  463. /* Write the section list, i.e. the references to the sections. Each
  464. * entry in the section list consist of the section's class ID and the
  465. * section's offset relative to the beginning of the stream. */
  466. int secCnt = 0;
  467. for (final Section section : getSections()) {
  468. final ClassID formatID = section.getFormatID();
  469. if (formatID == null) {
  470. throw new NoFormatIDException();
  471. }
  472. putClassId(bos, formatID);
  473. offsets[secCnt++][0] = bos.size();
  474. // offset dummy - filled later
  475. leos.writeInt(-1);
  476. }
  477. /* Write the sections themselves. */
  478. secCnt = 0;
  479. for (final Section section : getSections()) {
  480. offsets[secCnt++][1] = bos.size();
  481. section.write(bos);
  482. }
  483. byte[] result = bos.toByteArray();
  484. for (int[] off : offsets) {
  485. LittleEndian.putInt(result, off[0], off[1]);
  486. }
  487. return result;
  488. }
  489. }
  490. /**
  491. * Writes a property set to a document in a POI filesystem directory.
  492. *
  493. * @param dir The directory in the POI filesystem to write the document to.
  494. * @param name The document's name. If there is already a document with the
  495. * same name in the directory the latter will be overwritten.
  496. *
  497. * @throws WritingNotSupportedException if the filesystem doesn't support writing
  498. * @throws IOException if the old entry can't be deleted or the new entry be written
  499. */
  500. public void write(final DirectoryEntry dir, final String name)
  501. throws WritingNotSupportedException, IOException {
  502. /* If there is already an entry with the same name, remove it. */
  503. if (dir.hasEntry(name)) {
  504. final Entry e = dir.getEntry(name);
  505. e.delete();
  506. }
  507. /* Create the new entry. */
  508. dir.createDocument(name, toInputStream());
  509. }
  510. /**
  511. * Returns the contents of this property set stream as an input stream.
  512. * The latter can be used for example to write the property set into a POIFS
  513. * document. The input stream represents a snapshot of the property set.
  514. * If the latter is modified while the input stream is still being
  515. * read, the modifications will not be reflected in the input stream but in
  516. * the PropertySet only.
  517. *
  518. * @return the contents of this property set stream
  519. *
  520. * @throws WritingNotSupportedException if HPSF does not yet support writing
  521. * of a property's variant type.
  522. * @throws IOException if an I/O exception occurs.
  523. */
  524. public InputStream toInputStream() throws WritingNotSupportedException, IOException {
  525. return new UnsynchronizedByteArrayInputStream(toBytes());
  526. }
  527. /**
  528. * Fetches the property with the given ID, then does its
  529. * best to return it as a String
  530. *
  531. * @param propertyId the property id
  532. *
  533. * @return The property as a String, or null if unavailable
  534. */
  535. String getPropertyStringValue(final int propertyId) {
  536. Object propertyValue = getProperty(propertyId);
  537. return getPropertyStringValue(propertyValue);
  538. }
  539. /**
  540. * Return the string representation of a property value
  541. *
  542. * @param propertyValue the property value
  543. *
  544. * @return The property value as a String, or null if unavailable
  545. */
  546. public static String getPropertyStringValue(final Object propertyValue) {
  547. // Normal cases
  548. if (propertyValue == null) {
  549. return null;
  550. }
  551. if (propertyValue instanceof String) {
  552. return (String)propertyValue;
  553. }
  554. // Do our best with some edge cases
  555. if (propertyValue instanceof byte[]) {
  556. byte[] b = (byte[])propertyValue;
  557. switch (b.length) {
  558. case 0:
  559. return "";
  560. case 1:
  561. return Byte.toString(b[0]);
  562. case 2:
  563. return Integer.toString( LittleEndian.getUShort(b) );
  564. case 4:
  565. return Long.toString( LittleEndian.getUInt(b) );
  566. default:
  567. // Maybe it's a string? who knows!
  568. try {
  569. return CodePageUtil.getStringFromCodePage(b, Property.DEFAULT_CODEPAGE);
  570. } catch (UnsupportedEncodingException e) {
  571. // doesn't happen ...
  572. return "";
  573. }
  574. }
  575. }
  576. return propertyValue.toString();
  577. }
  578. /**
  579. * Checks whether this PropertySet represents a Summary Information.
  580. *
  581. * @return {@code true} if this PropertySet
  582. * represents a Summary Information, else {@code false}.
  583. */
  584. public boolean isSummaryInformation() {
  585. return !sections.isEmpty() && matchesSummary(getFirstSection().getFormatID(), SummaryInformation.FORMAT_ID);
  586. }
  587. /**
  588. * Checks whether this PropertySet is a Document Summary Information.
  589. *
  590. * @return {@code true} if this PropertySet
  591. * represents a Document Summary Information, else {@code false}.
  592. */
  593. public boolean isDocumentSummaryInformation() {
  594. return !sections.isEmpty() && matchesSummary(getFirstSection().getFormatID(), DocumentSummaryInformation.FORMAT_ID);
  595. }
  596. /* package */ static boolean matchesSummary(ClassID actual, ClassID... expected) {
  597. for (ClassID sum : expected) {
  598. if (sum.equals(actual) || sum.equalsInverted(actual)) {
  599. return true;
  600. }
  601. }
  602. return false;
  603. }
  604. /**
  605. * Convenience method returning the {@link Property} array contained in this
  606. * property set. It is a shortcut for getting he PropertySets
  607. * {@link Section}s list and then getting the {@link Property} array from the
  608. * first {@link Section}.
  609. *
  610. * @return The properties of the only {@link Section} of this
  611. * PropertySet.
  612. * @throws NoSingleSectionException if the PropertySet has
  613. * more or less than one {@link Section}.
  614. */
  615. public Property[] getProperties() throws NoSingleSectionException {
  616. return getFirstSection().getProperties();
  617. }
  618. /**
  619. * Convenience method returning the value of the property with the specified ID.
  620. * If the property is not available, {@code null} is returned and a subsequent
  621. * call to {@link #wasNull} will return {@code true}.
  622. *
  623. * @param id The property ID
  624. * @return The property value
  625. * @throws NoSingleSectionException if the PropertySet has
  626. * more or less than one {@link Section}.
  627. */
  628. protected Object getProperty(final int id) throws NoSingleSectionException {
  629. return getFirstSection().getProperty(id);
  630. }
  631. /**
  632. * Convenience method returning the value of a boolean property with the
  633. * specified ID. If the property is not available, {@code false} is returned.
  634. * A subsequent call to {@link #wasNull} will return {@code true} to let the
  635. * caller distinguish that case from a real property value of {@code false}.
  636. *
  637. * @param id The property ID
  638. * @return The property value
  639. * @throws NoSingleSectionException if the PropertySet has
  640. * more or less than one {@link Section}.
  641. */
  642. boolean getPropertyBooleanValue(final int id) throws NoSingleSectionException {
  643. return getFirstSection().getPropertyBooleanValue(id);
  644. }
  645. /**
  646. * Convenience method returning the value of the numeric
  647. * property with the specified ID. If the property is not
  648. * available, 0 is returned. A subsequent call to {@link #wasNull}
  649. * will return {@code true} to let the caller distinguish
  650. * that case from a real property value of 0.
  651. *
  652. * @param id The property ID
  653. * @return The propertyIntValue value
  654. * @throws NoSingleSectionException if the PropertySet has
  655. * more or less than one {@link Section}.
  656. */
  657. int getPropertyIntValue(final int id) throws NoSingleSectionException {
  658. return getFirstSection().getPropertyIntValue(id);
  659. }
  660. /**
  661. * Checks whether the property which the last call to {@link
  662. * #getPropertyIntValue} or {@link #getProperty} tried to access
  663. * was available or not. This information might be important for
  664. * callers of {@link #getPropertyIntValue} since the latter
  665. * returns 0 if the property does not exist. Using wasNull,
  666. * the caller can distinguish this case from a
  667. * property's real value of 0.
  668. *
  669. * @return {@code true} if the last call to {@link
  670. * #getPropertyIntValue} or {@link #getProperty} tried to access a
  671. * property that was not available, else {@code false}.
  672. * @throws NoSingleSectionException if the PropertySet has
  673. * more than one {@link Section}.
  674. */
  675. public boolean wasNull() throws NoSingleSectionException {
  676. return getFirstSection().wasNull();
  677. }
  678. /**
  679. * Gets the PropertySets first section.
  680. *
  681. * @return The PropertySets first section.
  682. */
  683. @SuppressWarnings("WeakerAccess")
  684. public Section getFirstSection() {
  685. if (sections.isEmpty()) {
  686. throw new MissingSectionException("Property set does not contain any sections.");
  687. }
  688. return sections.get(0);
  689. }
  690. /**
  691. * Returns {@code true} if the {@code PropertySet} is equal
  692. * to the specified parameter, else {@code false}.
  693. *
  694. * @param o the object to compare this {@code PropertySet} with
  695. *
  696. * @return {@code true} if the objects are equal, {@code false}
  697. * if not
  698. */
  699. @Override
  700. public boolean equals(final Object o) {
  701. if (!(o instanceof PropertySet)) {
  702. return false;
  703. }
  704. final PropertySet ps = (PropertySet) o;
  705. int byteOrder1 = ps.getByteOrder();
  706. int byteOrder2 = getByteOrder();
  707. ClassID classID1 = ps.getClassID();
  708. ClassID classID2 = getClassID();
  709. int format1 = ps.getFormat();
  710. int format2 = getFormat();
  711. int osVersion1 = ps.getOSVersion();
  712. int osVersion2 = getOSVersion();
  713. int sectionCount1 = ps.getSectionCount();
  714. int sectionCount2 = getSectionCount();
  715. if (byteOrder1 != byteOrder2 ||
  716. !classID1.equals(classID2) ||
  717. format1 != format2 ||
  718. osVersion1 != osVersion2 ||
  719. sectionCount1 != sectionCount2) {
  720. return false;
  721. }
  722. /* Compare the sections: */
  723. return getSections().containsAll(ps.getSections());
  724. }
  725. @NotImplemented
  726. @Override
  727. public int hashCode() {
  728. throw new UnsupportedOperationException("FIXME: Not yet implemented.");
  729. }
  730. @Override
  731. public String toString() {
  732. final StringBuilder b = new StringBuilder();
  733. final int sectionCount = getSectionCount();
  734. b.append(getClass().getName());
  735. b.append('[');
  736. b.append("byteOrder: ");
  737. b.append(getByteOrder());
  738. b.append(", classID: ");
  739. b.append(getClassID());
  740. b.append(", format: ");
  741. b.append(getFormat());
  742. b.append(", OSVersion: ");
  743. b.append(getOSVersion());
  744. b.append(", sectionCount: ");
  745. b.append(sectionCount);
  746. b.append(", sections: [\n");
  747. for (Section section: getSections()) {
  748. b.append(section.toString(getPropertySetIDMap()));
  749. }
  750. b.append(']');
  751. b.append(']');
  752. return b.toString();
  753. }
  754. void remove1stProperty(long id) {
  755. getFirstSection().removeProperty(id);
  756. }
  757. void set1stProperty(long id, String value) {
  758. getFirstSection().setProperty((int)id, value);
  759. }
  760. void set1stProperty(long id, int value) {
  761. getFirstSection().setProperty((int)id, value);
  762. }
  763. void set1stProperty(long id, boolean value) {
  764. getFirstSection().setProperty((int)id, value);
  765. }
  766. @SuppressWarnings("SameParameterValue")
  767. void set1stProperty(long id, byte[] value) {
  768. getFirstSection().setProperty((int)id, value);
  769. }
  770. private static void putClassId(final UnsynchronizedByteArrayOutputStream out, final ClassID n) {
  771. byte[] b = new byte[16];
  772. n.write(b, 0);
  773. out.write(b, 0, b.length);
  774. }
  775. }