View Javadoc
1   /*
2   Copyright (c) 2013 James Ahlborn
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.Closeable;
21  import java.io.FileInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.nio.ByteBuffer;
26  import java.nio.charset.Charset;
27  import java.nio.charset.StandardCharsets;
28  import java.sql.Blob;
29  import java.sql.SQLException;
30  import java.sql.SQLFeatureNotSupportedException;
31  import java.text.Normalizer;
32  import java.util.EnumSet;
33  import java.util.Set;
34  import java.util.regex.Pattern;
35  
36  import com.healthmarketscience.jackcess.DataType;
37  import com.healthmarketscience.jackcess.util.OleBlob;
38  import static com.healthmarketscience.jackcess.util.OleBlob.*;
39  import org.apache.commons.lang3.builder.ToStringBuilder;
40  
41  /**
42   * Utility code for working with OLE data.
43   *
44   * @author James Ahlborn
45   * @usage _advanced_class_
46   */
47  public class OleUtil
48  {
49    /**
50     * Interface used to allow optional inclusion of the poi library for working
51     * with compound ole data.
52     */
53    interface CompoundPackageFactory
54    {
55      public ContentImpl createCompoundPackageContent(
56          OleBlobImpl blob, String prettyName, String className, String typeName,
57          ByteBuffer blobBb, int dataBlockLen);
58    }
59  
60    private static final int PACKAGE_SIGNATURE = 0x1C15;
61    private static final Charset OLE_CHARSET = StandardCharsets.US_ASCII;
62    private static final Charset OLE_UTF_CHARSET = StandardCharsets.UTF_16LE;
63    private static final byte[] COMPOUND_STORAGE_SIGNATURE =
64      {(byte)0xd0,(byte)0xcf,(byte)0x11,(byte)0xe0,
65       (byte)0xa1,(byte)0xb1,(byte)0x1a,(byte)0xe1};
66    private static final String SIMPLE_PACKAGE_TYPE = "Package";
67    private static final int PACKAGE_OBJECT_TYPE = 0x02;
68    private static final int OLE_VERSION = 0x0501;
69    private static final int OLE_FORMAT = 0x02;
70    private static final int PACKAGE_STREAM_SIGNATURE = 0x02;
71    private static final int PS_EMBEDDED_FILE = 0x030000;
72    private static final int PS_LINKED_FILE = 0x010000;
73    private static final Set<ContentType> WRITEABLE_TYPES = EnumSet.of(
74        ContentType.LINK, ContentType.SIMPLE_PACKAGE, ContentType.OTHER);
75    private static final byte[] NO_DATA = new byte[0];
76    private static final int LINK_HEADER = 0x01;
77    private static final byte[] PACKAGE_FOOTER = {
78      0x01, 0x05, 0x00, 0x00, 0x00, 0x00,
79      0x00, 0x00, 0x01, (byte)0xAD, 0x05, (byte)0xFE
80    };
81  
82    // regex pattern which matches all the crazy extra stuff in unicode
83    private static final Pattern UNICODE_ACCENT_PATTERN =
84      Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
85  
86    private static final CompoundPackageFactory COMPOUND_FACTORY;
87  
88    static {
89      CompoundPackageFactory compoundFactory = null;
90      try {
91        compoundFactory = (CompoundPackageFactory)
92          Class.forName("com.healthmarketscience.jackcess.impl.CompoundOleUtil")
93          .newInstance();
94      } catch(Throwable t) {
95        // must not have poi, will load compound ole data as "other"
96      }
97      COMPOUND_FACTORY = compoundFactory;
98    }
99  
100   /**
101    * Parses an access database blob structure and returns an appropriate
102    * OleBlob instance.
103    */
104   public static OleBlob parseBlob(byte[] bytes) {
105     return new OleBlobImpl(bytes);
106   }
107 
108   /**
109    * Creates a new OlBlob instance using the given information.
110    */
111   public static OleBlob createBlob(Builder oleBuilder)
112     throws IOException
113   {
114     try {
115 
116       if(!WRITEABLE_TYPES.contains(oleBuilder.getType())) {
117         throw new IllegalArgumentException(
118             "Cannot currently create ole values of type " +
119             oleBuilder.getType());
120       }
121 
122       long contentLen = oleBuilder.getContentLength();
123       byte[] contentBytes = oleBuilder.getBytes();
124       InputStream contentStream = oleBuilder.getStream();
125       byte[] packageStreamHeader = NO_DATA;
126       byte[] packageStreamFooter = NO_DATA;
127 
128       switch(oleBuilder.getType()) {
129       case LINK:
130         packageStreamHeader = writePackageStreamHeader(oleBuilder);
131 
132         // link "content" is file path
133         contentBytes = getZeroTermStrBytes(oleBuilder.getFilePath());
134         contentLen = contentBytes.length;
135         break;
136 
137       case SIMPLE_PACKAGE:
138         packageStreamHeader = writePackageStreamHeader(oleBuilder);
139         packageStreamFooter = writePackageStreamFooter(oleBuilder);
140         break;
141 
142       case OTHER:
143         // nothing more to do
144         break;
145       default:
146         throw new RuntimeException("unexpected type " + oleBuilder.getType());
147       }
148 
149       long payloadLen = packageStreamHeader.length + packageStreamFooter.length +
150         contentLen;
151       byte[] packageHeader = writePackageHeader(oleBuilder, payloadLen);
152 
153       long totalOleLen = packageHeader.length + PACKAGE_FOOTER.length +
154         payloadLen;
155       if(totalOleLen > DataType.OLE.getMaxSize()) {
156         throw new IllegalArgumentException("Content size of " + totalOleLen +
157                                            " is too large for ole column");
158       }
159 
160       byte[] oleBytes = new byte[(int)totalOleLen];
161       ByteBuffer bb = PageChannel.wrap(oleBytes);
162       bb.put(packageHeader);
163       bb.put(packageStreamHeader);
164 
165       if(contentLen > 0L) {
166         if(contentBytes != null) {
167           bb.put(contentBytes);
168         } else {
169           byte[] buf = new byte[8192];
170           int numBytes = 0;
171           while((numBytes = contentStream.read(buf)) >= 0) {
172             bb.put(buf, 0, numBytes);
173           }
174         }
175       }
176 
177       bb.put(packageStreamFooter);
178       bb.put(PACKAGE_FOOTER);
179 
180       return parseBlob(oleBytes);
181 
182     } finally {
183       ByteUtil.closeQuietly(oleBuilder.getStream());
184     }
185   }
186 
187   private static byte[] writePackageHeader(Builder oleBuilder,
188                                            long contentLen) {
189 
190     byte[] prettyNameBytes = getZeroTermStrBytes(oleBuilder.getPrettyName());
191     String className = oleBuilder.getClassName();
192     String typeName = oleBuilder.getTypeName();
193     if(className == null) {
194       className = typeName;
195     } else if(typeName == null) {
196       typeName = className;
197     }
198     byte[] classNameBytes = getZeroTermStrBytes(className);
199     byte[] typeNameBytes = getZeroTermStrBytes(typeName);
200 
201     int packageHeaderLen = 20 + prettyNameBytes.length + classNameBytes.length;
202 
203     int oleHeaderLen = 24 + typeNameBytes.length;
204 
205     byte[] headerBytes = new byte[packageHeaderLen + oleHeaderLen];
206 
207     ByteBuffer bb = PageChannel.wrap(headerBytes);
208 
209     // write outer package header
210     bb.putShort((short)PACKAGE_SIGNATURE);
211     bb.putShort((short)packageHeaderLen);
212     bb.putInt(PACKAGE_OBJECT_TYPE);
213     bb.putShort((short)prettyNameBytes.length);
214     bb.putShort((short)classNameBytes.length);
215     int prettyNameOff = bb.position() + 8;
216     bb.putShort((short)prettyNameOff);
217     bb.putShort((short)(prettyNameOff + prettyNameBytes.length));
218     bb.putInt(-1);
219     bb.put(prettyNameBytes);
220     bb.put(classNameBytes);
221 
222     // put ole header
223     bb.putInt(OLE_VERSION);
224     bb.putInt(OLE_FORMAT);
225     bb.putInt(typeNameBytes.length);
226     bb.put(typeNameBytes);
227     bb.putLong(0L);
228     bb.putInt((int)contentLen);
229 
230     return headerBytes;
231   }
232 
233   private static byte[] writePackageStreamHeader(Builder oleBuilder) {
234 
235     byte[] fileNameBytes = getZeroTermStrBytes(oleBuilder.getFileName());
236     byte[] filePathBytes = getZeroTermStrBytes(oleBuilder.getFilePath());
237 
238     int headerLen = 6 + fileNameBytes.length + filePathBytes.length;
239 
240     if(oleBuilder.getType() == ContentType.SIMPLE_PACKAGE) {
241 
242       headerLen += 8 + filePathBytes.length;
243 
244     } else {
245 
246       headerLen += 2;
247     }
248 
249     byte[] headerBytes = new byte[headerLen];
250     ByteBuffer bb = PageChannel.wrap(headerBytes);
251     bb.putShort((short)PACKAGE_STREAM_SIGNATURE);
252     bb.put(fileNameBytes);
253     bb.put(filePathBytes);
254 
255     if(oleBuilder.getType() == ContentType.SIMPLE_PACKAGE) {
256       bb.putInt(PS_EMBEDDED_FILE);
257       bb.putInt(filePathBytes.length);
258       bb.put(filePathBytes, 0, filePathBytes.length);
259       bb.putInt((int) oleBuilder.getContentLength());
260     } else {
261       bb.putInt(PS_LINKED_FILE);
262       bb.putShort((short)LINK_HEADER);
263     }
264 
265     return headerBytes;
266   }
267 
268   private static byte[] writePackageStreamFooter(Builder oleBuilder) {
269 
270     // note, these are _not_ zero terminated
271     byte[] fileNameBytes = oleBuilder.getFileName().getBytes(OLE_UTF_CHARSET);
272     byte[] filePathBytes = oleBuilder.getFilePath().getBytes(OLE_UTF_CHARSET);
273 
274     int footerLen = 12 + (filePathBytes.length * 2) + fileNameBytes.length;
275 
276     byte[] footerBytes = new byte[footerLen];
277     ByteBuffer bb = PageChannel.wrap(footerBytes);
278 
279     bb.putInt(filePathBytes.length/2);
280     bb.put(filePathBytes);
281     bb.putInt(fileNameBytes.length/2);
282     bb.put(fileNameBytes);
283     bb.putInt(filePathBytes.length/2);
284     bb.put(filePathBytes);
285 
286     return footerBytes;
287   }
288 
289   /**
290    * creates the appropriate ContentImpl for the given blob.
291    */
292   private static ContentImpl parseContent(OleBlobImpl blob)
293     throws IOException
294   {
295     ByteBuffer bb = PageChannel.wrap(blob.getBytes());
296 
297     if((bb.remaining() < 2) || (bb.getShort() != PACKAGE_SIGNATURE)) {
298       return new UnknownContentImpl(blob);
299     }
300 
301     // read outer package header
302     int headerSize = bb.getShort();
303     /* int objType = */ bb.getInt();
304     int prettyNameLen = bb.getShort();
305     int classNameLen = bb.getShort();
306     int prettyNameOff = bb.getShort();
307     int classNameOff = bb.getShort();
308     /* int objSize = */ bb.getInt();
309     String prettyName = readStr(bb, prettyNameOff, prettyNameLen);
310     String className = readStr(bb, classNameOff, classNameLen);
311     bb.position(headerSize);
312 
313     // read ole header
314     int oleVer = bb.getInt();
315     /* int format = */ bb.getInt();
316 
317     if(oleVer != OLE_VERSION) {
318       return new UnknownContentImpl(blob);
319     }
320 
321     int typeNameLen = bb.getInt();
322     String typeName = readStr(bb, bb.position(), typeNameLen);
323     bb.getLong(); // unused
324     int dataBlockLen = bb.getInt();
325     int dataBlockPos = bb.position();
326 
327 
328     if(SIMPLE_PACKAGE_TYPE.equalsIgnoreCase(typeName)) {
329       return createSimplePackageContent(
330           blob, prettyName, className, typeName, bb, dataBlockLen);
331     }
332 
333     // if COMPOUND_FACTORY is null, the poi library isn't available, so just
334     // load compound data as "other"
335     if((COMPOUND_FACTORY != null) &&
336        (bb.remaining() >= COMPOUND_STORAGE_SIGNATURE.length) &&
337        ByteUtil.matchesRange(bb, bb.position(), COMPOUND_STORAGE_SIGNATURE)) {
338       return COMPOUND_FACTORY.createCompoundPackageContent(
339           blob, prettyName, className, typeName, bb, dataBlockLen);
340     }
341 
342     // this is either some other "special" (as yet unhandled) format, or it is
343     // simply an embedded file (or it is compound data and poi isn't available)
344     return new OtherContentImpl(blob, prettyName, className,
345                                 typeName, dataBlockPos, dataBlockLen);
346   }
347 
348   private static ContentImpl createSimplePackageContent(
349       OleBlobImpl blob, String prettyName, String className, String typeName,
350       ByteBuffer blobBb, int dataBlockLen) {
351 
352     int dataBlockPos = blobBb.position();
353     ByteBuffer bb = PageChannel.narrowBuffer(blobBb, dataBlockPos,
354                                              dataBlockPos + dataBlockLen);
355 
356     int packageSig = bb.getShort();
357     if(packageSig != PACKAGE_STREAM_SIGNATURE) {
358       return new OtherContentImpl(blob, prettyName, className,
359                                   typeName, dataBlockPos, dataBlockLen);
360     }
361 
362     String fileName = readZeroTermStr(bb);
363     String filePath = readZeroTermStr(bb);
364     int packageType = bb.getInt();
365 
366     if(packageType == PS_EMBEDDED_FILE) {
367 
368       int localFilePathLen = bb.getInt();
369       String localFilePath = readStr(bb, bb.position(), localFilePathLen);
370       int dataLen = bb.getInt();
371       int dataPos = bb.position();
372       bb.position(dataLen + dataPos);
373 
374       // remaining strings are in "reverse" order (local file path, file name,
375       // file path).  these string usee a real utf charset, and therefore can
376       // "fix" problems with ascii based names (so we prefer these strings to
377       // the original strings we found)
378       int strNum = 0;
379       while(true) {
380 
381         int rem = bb.remaining();
382         if(rem < 4) {
383           break;
384         }
385 
386         int strLen = bb.getInt();
387         String remStr = readStr(bb, bb.position(), strLen * 2, OLE_UTF_CHARSET);
388 
389         switch(strNum) {
390         case 0:
391           localFilePath = remStr;
392           break;
393         case 1:
394           fileName = remStr;
395           break;
396         case 2:
397           filePath = remStr;
398           break;
399         default:
400           // ignore
401         }
402 
403         ++strNum;
404       }
405 
406       return new SimplePackageContentImpl(
407           blob, prettyName, className, typeName, dataPos, dataLen,
408           fileName, filePath, localFilePath);
409     }
410 
411     if(packageType == PS_LINKED_FILE) {
412 
413       bb.getShort(); //unknown
414       String linkStr = readZeroTermStr(bb);
415 
416       return new LinkContentImpl(blob, prettyName, className, typeName,
417                                  fileName, linkStr, filePath);
418     }
419 
420     return new OtherContentImpl(blob, prettyName, className,
421                                 typeName, dataBlockPos, dataBlockLen);
422   }
423 
424   private static String readStr(ByteBuffer bb, int off, int len) {
425     return readStr(bb, off, len, OLE_CHARSET);
426   }
427 
428   private static String readZeroTermStr(ByteBuffer bb) {
429     int off = bb.position();
430     while(bb.hasRemaining()) {
431       byte b = bb.get();
432       if(b == 0) {
433         break;
434       }
435     }
436     int len = bb.position() - off;
437     return readStr(bb, off, len);
438   }
439 
440   private static String readStr(ByteBuffer bb, int off, int len,
441                                 Charset charset) {
442     String str = new String(bb.array(), off, len, charset);
443     bb.position(off + len);
444     if(str.charAt(str.length() - 1) == '\0') {
445       str = str.substring(0, str.length() - 1);
446     }
447     return str;
448   }
449 
450   private static byte[] getZeroTermStrBytes(String str) {
451     // since we are converting to ascii, try to make "nicer" versions of crazy
452     // chars (e.g. convert "u with an umlaut" to just "u").  this may not
453     // ultimately help anything but it is what ms access does.
454 
455     // decompose complex chars into combos of char and accent
456     str = Normalizer.normalize(str, Normalizer.Form.NFD);
457     // strip the accents
458     str = UNICODE_ACCENT_PATTERN.matcher(str).replaceAll("");
459     // (re)normalize what is left
460     str = Normalizer.normalize(str, Normalizer.Form.NFC);
461 
462     return (str + '\0').getBytes(OLE_CHARSET);
463   }
464 
465 
466   static final class OleBlobImpl implements OleBlob, ColumnImpl.InMemoryBlob
467   {
468     private byte[] _bytes;
469     private ContentImpl _content;
470 
471     private OleBlobImpl(byte[] bytes) {
472       _bytes = bytes;
473     }
474 
475     @Override
476     public void writeTo(OutputStream out) throws IOException {
477       out.write(_bytes);
478     }
479 
480     @Override
481     public Content getContent() throws IOException {
482       if(_content == null) {
483         _content = parseContent(this);
484       }
485       return _content;
486     }
487 
488     @Override
489     public InputStream getBinaryStream() throws SQLException {
490       return new ByteArrayInputStream(_bytes);
491     }
492 
493     @Override
494     public InputStream getBinaryStream(long pos, long len)
495       throws SQLException
496     {
497       return new ByteArrayInputStream(_bytes, fromJdbcOffset(pos), (int)len);
498     }
499 
500     @Override
501     public long length() throws SQLException {
502       return _bytes.length;
503     }
504 
505     @Override
506     public byte[] getBytes() throws IOException {
507       if(_bytes == null) {
508         throw new IOException("blob is closed");
509       }
510       return _bytes;
511     }
512 
513     @Override
514     public byte[] getBytes(long pos, int len) throws SQLException {
515       return ByteUtil.copyOf(_bytes, fromJdbcOffset(pos), len);
516     }
517 
518     @Override
519     public long position(byte[] pattern, long start) throws SQLException {
520       int pos = ByteUtil.findRange(PageChannel.wrap(_bytes),
521                                    fromJdbcOffset(start), pattern);
522       return((pos >= 0) ? toJdbcOffset(pos) : pos);
523     }
524 
525     @Override
526     public long position(Blob pattern, long start) throws SQLException {
527       return position(pattern.getBytes(1L, (int)pattern.length()), start);
528     }
529 
530     @Override
531     public OutputStream setBinaryStream(long position) throws SQLException {
532       throw new SQLFeatureNotSupportedException();
533     }
534 
535     @Override
536     public void truncate(long len) throws SQLException {
537       throw new SQLFeatureNotSupportedException();
538     }
539 
540     @Override
541     public int setBytes(long pos, byte[] bytes) throws SQLException {
542       throw new SQLFeatureNotSupportedException();
543     }
544 
545     @Override
546     public int setBytes(long pos, byte[] bytes, int offset, int lesn)
547       throws SQLException {
548       throw new SQLFeatureNotSupportedException();
549     }
550 
551     @Override
552     public void free() {
553       close();
554     }
555 
556     @Override
557     public void close() {
558       _bytes = null;
559       ByteUtil.closeQuietly(_content);
560       _content = null;
561     }
562 
563     private static int toJdbcOffset(int off) {
564       return off + 1;
565     }
566 
567     private static int fromJdbcOffset(long off) {
568       return (int)off - 1;
569     }
570 
571     @Override
572     public String toString() {
573       ToStringBuilder sb = CustomToStringStyle.builder(this);
574       if(_content != null) {
575         sb.append("content", _content);
576       } else {
577         sb.append("bytes", _bytes);
578         sb.append("content", "(uninitialized)");
579       }
580       return sb.toString();
581     }
582   }
583 
584   static abstract class ContentImpl implements Content, Closeable
585   {
586     protected final OleBlobImpl _blob;
587 
588     protected ContentImpl(OleBlobImpl blob) {
589       _blob = blob;
590     }
591 
592     @Override
593     public OleBlobImpl getBlob() {
594       return _blob;
595     }
596 
597     protected byte[] getBytes() throws IOException {
598       return getBlob().getBytes();
599     }
600 
601     @Override
602     public void close() {
603       // base does nothing
604     }
605 
606     protected ToStringBuilder toString(ToStringBuilder sb) {
607       sb.append("type", getType());
608       return sb;
609     }
610   }
611 
612   static abstract class EmbeddedContentImpl extends ContentImpl
613     implements EmbeddedContent
614   {
615     private final int _position;
616     private final int _length;
617 
618     protected EmbeddedContentImpl(OleBlobImpl blob, int position, int length)
619     {
620       super(blob);
621       _position = position;
622       _length = length;
623     }
624 
625     @Override
626     public long length() {
627       return _length;
628     }
629 
630     @Override
631     public InputStream getStream() throws IOException {
632       return new ByteArrayInputStream(getBytes(), _position, _length);
633     }
634 
635     @Override
636     public void writeTo(OutputStream out) throws IOException {
637       out.write(getBytes(), _position, _length);
638     }
639 
640     @Override
641     protected ToStringBuilder toString(ToStringBuilder sb) {
642       super.toString(sb);
643       if(_position >= 0) {
644         sb.append("content", ByteBuffer.wrap(_blob._bytes, _position, _length));
645       }
646       return sb;
647     }
648   }
649 
650   static abstract class EmbeddedPackageContentImpl
651     extends EmbeddedContentImpl
652     implements PackageContent
653   {
654     private final String _prettyName;
655     private final String _className;
656     private final String _typeName;
657 
658     protected EmbeddedPackageContentImpl(
659         OleBlobImpl blob, String prettyName, String className,
660         String typeName, int position, int length)
661     {
662       super(blob, position, length);
663       _prettyName = prettyName;
664       _className = className;
665       _typeName = typeName;
666     }
667 
668     @Override
669     public String getPrettyName() {
670       return _prettyName;
671     }
672 
673     @Override
674     public String getClassName() {
675       return _className;
676     }
677 
678     @Override
679     public String getTypeName() {
680       return _typeName;
681     }
682 
683     @Override
684     protected ToStringBuilder toString(ToStringBuilder sb) {
685       sb.append("prettyName", _prettyName)
686         .append("className", _className)
687         .append("typeName", _typeName);
688       super.toString(sb);
689       return sb;
690     }
691   }
692 
693   private static final class LinkContentImpl
694     extends EmbeddedPackageContentImpl
695     implements LinkContent
696   {
697     private final String _fileName;
698     private final String _linkPath;
699     private final String _filePath;
700 
701     private LinkContentImpl(OleBlobImpl blob, String prettyName,
702                             String className, String typeName,
703                             String fileName, String linkPath,
704                             String filePath)
705     {
706       super(blob, prettyName, className, typeName, -1, -1);
707       _fileName = fileName;
708       _linkPath = linkPath;
709       _filePath = filePath;
710     }
711 
712     @Override
713     public ContentType getType() {
714       return ContentType.LINK;
715     }
716 
717     @Override
718     public String getFileName() {
719       return _fileName;
720     }
721 
722     @Override
723     public String getLinkPath() {
724       return _linkPath;
725     }
726 
727     @Override
728     public String getFilePath() {
729       return _filePath;
730     }
731 
732     @Override
733     public InputStream getLinkStream() throws IOException {
734       return new FileInputStream(getLinkPath());
735     }
736 
737     @Override
738     public String toString() {
739       return toString(CustomToStringStyle.builder(this))
740         .append("fileName", _fileName)
741         .append("linkPath", _linkPath)
742         .append("filePath", _filePath)
743         .toString();
744     }
745   }
746 
747   private static final class SimplePackageContentImpl
748     extends EmbeddedPackageContentImpl
749     implements SimplePackageContent
750   {
751     private final String _fileName;
752     private final String _filePath;
753     private final String _localFilePath;
754 
755     private SimplePackageContentImpl(OleBlobImpl blob, String prettyName,
756                                      String className, String typeName,
757                                      int position, int length,
758                                      String fileName, String filePath,
759                                      String localFilePath)
760     {
761       super(blob, prettyName, className, typeName, position, length);
762       _fileName = fileName;
763       _filePath = filePath;
764       _localFilePath = localFilePath;
765     }
766 
767     @Override
768     public ContentType getType() {
769       return ContentType.SIMPLE_PACKAGE;
770     }
771 
772     @Override
773     public String getFileName() {
774       return _fileName;
775     }
776 
777     @Override
778     public String getFilePath() {
779       return _filePath;
780     }
781 
782     @Override
783     public String getLocalFilePath() {
784       return _localFilePath;
785     }
786 
787     @Override
788     public String toString() {
789       return toString(CustomToStringStyle.builder(this))
790         .append("fileName", _fileName)
791         .append("filePath", _filePath)
792         .append("localFilePath", _localFilePath)
793         .toString();
794     }
795   }
796 
797   private static final class OtherContentImpl
798     extends EmbeddedPackageContentImpl
799     implements OtherContent
800   {
801     private OtherContentImpl(
802         OleBlobImpl blob, String prettyName, String className,
803         String typeName, int position, int length)
804     {
805       super(blob, prettyName, className, typeName, position, length);
806     }
807 
808     @Override
809     public ContentType getType() {
810       return ContentType.OTHER;
811     }
812 
813     @Override
814     public String toString() {
815       return toString(CustomToStringStyle.builder(this))
816         .toString();
817     }
818   }
819 
820   private static final class UnknownContentImpl extends ContentImpl
821   {
822     private UnknownContentImpl(OleBlobImpl blob) {
823       super(blob);
824     }
825 
826     @Override
827     public ContentType getType() {
828       return ContentType.UNKNOWN;
829     }
830 
831     @Override
832     public String toString() {
833       return toString(CustomToStringStyle.builder(this))
834         .append("content", _blob._bytes)
835         .toString();
836     }
837   }
838 
839 }