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