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 23KB

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