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.FileNotFoundException;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.OutputStream;
23  import java.io.UnsupportedEncodingException;
24  import java.net.URLDecoder;
25  import java.net.URLEncoder;
26  import java.nio.ByteBuffer;
27  import java.util.ArrayList;
28  import java.util.Iterator;
29  import java.util.List;
30  
31  import com.healthmarketscience.jackcess.RuntimeIOException;
32  import static com.healthmarketscience.jackcess.impl.OleUtil.*;
33  import com.healthmarketscience.jackcess.util.MemFileChannel;
34  import static com.healthmarketscience.jackcess.util.OleBlob.*;
35  import org.apache.commons.lang.builder.ToStringBuilder;
36  import org.apache.poi.poifs.filesystem.DirectoryEntry;
37  import org.apache.poi.poifs.filesystem.DocumentEntry;
38  import org.apache.poi.poifs.filesystem.DocumentInputStream;
39  import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
40  
41  /**
42   * Utility code for working with OLE data which is in the compound storage
43   * format.  This functionality relies on the optional POI library.
44   * <p/> 
45   * Note that all POI usage is restricted to this file so that the basic ole
46   * support in OleUtil can be utilized without requiring POI.
47   *
48   * @author James Ahlborn
49   * @usage _advanced_class_
50   */
51  public class CompoundOleUtil implements OleUtil.CompoundPackageFactory
52  {
53    private static final String ENTRY_NAME_CHARSET = "UTF-8";
54    private static final String ENTRY_SEPARATOR = "/";
55    private static final String CONTENTS_ENTRY = "CONTENTS";
56  
57    static {
58      // force a poi class to be loaded to ensure that when this class is
59      // loaded, we know that the poi classes are available
60      NPOIFSFileSystem.class.getName();
61    }
62  
63    public CompoundOleUtil() 
64    {
65    }
66  
67    /**
68     * Creates a nes CompoundContent for the given blob information.
69     */
70    public ContentImpl createCompoundPackageContent(
71        OleBlobImpl blob, String prettyName, String className, String typeName,
72        ByteBuffer blobBb, int dataBlockLen)
73    {
74      return new CompoundContentImpl(blob, prettyName, className, typeName,
75                                     blobBb.position(), dataBlockLen);
76    }
77  
78    /**
79     * Gets a DocumentEntry from compound storage based on a fully qualified,
80     * encoded entry name.
81     *
82     * @param entryName fully qualified, encoded entry name
83     * @param dir root directory of the compound storage
84     *
85     * @return the relevant DocumentEntry
86     * @throws FileNotFoundException if the entry does not exist
87     * @throws IOException if some other io error occurs
88     */
89    public static DocumentEntry getDocumentEntry(String entryName,
90                                                 DirectoryEntry dir) 
91      throws IOException 
92    {
93      // split entry name into individual components and decode them
94      List<String> entryNames = new ArrayList<String>();
95      for(String str : entryName.split(ENTRY_SEPARATOR)) {
96        if(str.length() == 0) {
97          continue;
98        }
99        entryNames.add(decodeEntryName(str));
100     }
101 
102     DocumentEntry entry = null;
103     Iterator<String> iter = entryNames.iterator();
104     while(iter.hasNext()) {
105       org.apache.poi.poifs.filesystem.Entry tmpEntry = dir.getEntry(iter.next());
106       if(tmpEntry instanceof DirectoryEntry) {
107         dir = (DirectoryEntry)tmpEntry;
108       } else if(!iter.hasNext() && (tmpEntry instanceof DocumentEntry)) {
109         entry = (DocumentEntry)tmpEntry;
110       } else {
111         break;
112       }        
113     }
114       
115     if(entry == null) {
116       throw new FileNotFoundException("Could not find document " + entryName);
117     }
118 
119     return entry;
120   }
121 
122   private static String encodeEntryName(String name) {
123     try {
124       return URLEncoder.encode(name, ENTRY_NAME_CHARSET);
125     } catch(UnsupportedEncodingException e) {
126       throw new RuntimeException(e);
127     }
128   }
129 
130   private static String decodeEntryName(String name) {
131     try {
132       return URLDecoder.decode(name, ENTRY_NAME_CHARSET);
133     } catch(UnsupportedEncodingException e) {
134       throw new RuntimeException(e);
135     }
136   }
137 
138   private static final class CompoundContentImpl 
139     extends EmbeddedPackageContentImpl
140     implements CompoundContent
141   {
142     private NPOIFSFileSystem _fs;
143 
144     private CompoundContentImpl(
145         OleBlobImpl blob, String prettyName, String className,
146         String typeName, int position, int length) 
147     {
148       super(blob, prettyName, className, typeName, position, length);
149     }        
150 
151     public ContentType getType() {
152       return ContentType.COMPOUND_STORAGE;
153     }
154 
155     private NPOIFSFileSystem getFileSystem() throws IOException {
156       if(_fs == null) {
157         _fs = new NPOIFSFileSystem(MemFileChannel.newChannel(getStream(), "r"));
158       }
159       return _fs;
160     }
161 
162     public Iterator<Entry> iterator() {
163       try {
164       return getEntries(new ArrayList<Entry>(), getFileSystem().getRoot(),
165                         ENTRY_SEPARATOR).iterator();
166       } catch(IOException e) {
167         throw new RuntimeIOException(e);
168       }
169     }
170 
171     public EntryImpl getEntry(String entryName) throws IOException {
172       return new EntryImpl(entryName, 
173                            getDocumentEntry(entryName, getFileSystem().getRoot()));
174     }
175 
176     public boolean hasContentsEntry() throws IOException {
177       return getFileSystem().getRoot().hasEntry(CONTENTS_ENTRY);
178     }
179 
180     public EntryImpl getContentsEntry() throws IOException {
181       return getEntry(CONTENTS_ENTRY);
182     }
183 
184     private List<Entry> getEntries(List<Entry> entries, DirectoryEntry dir, 
185                                    String prefix) {
186       for(org.apache.poi.poifs.filesystem.Entry entry : dir) {
187         if (entry instanceof DirectoryEntry) {
188           // .. recurse into this directory
189           getEntries(entries, (DirectoryEntry)entry, prefix + ENTRY_SEPARATOR);
190         } else if(entry instanceof DocumentEntry) {
191           // grab the entry name/detils
192           DocumentEntry de = (DocumentEntry)entry;
193           String entryName = prefix + encodeEntryName(entry.getName());
194           entries.add(new EntryImpl(entryName, de));
195         }
196       }
197       return entries;
198     }
199 
200     @Override
201     public void close() {
202       ByteUtil.closeQuietly(_fs);
203       _fs = null;
204       super.close();
205     }
206 
207     @Override
208     public String toString() {
209       ToStringBuilder sb = toString(CustomToStringStyle.builder(this));
210 
211       try {
212         sb.append("hasContentsEntry", hasContentsEntry());
213         sb.append("entries", getEntries(new ArrayList<Entry>(), 
214                                         getFileSystem().getRoot(),
215                                         ENTRY_SEPARATOR));
216       } catch(IOException e) {  
217         sb.append("entries", "<" + e + ">");
218       }
219 
220       return sb.toString();
221     }
222 
223     private final class EntryImpl implements CompoundContent.Entry
224     {
225       private final String _name;
226       private final DocumentEntry _docEntry;
227 
228       private EntryImpl(String name, DocumentEntry docEntry) {
229         _name = name;
230         _docEntry = docEntry;
231       }
232 
233       public ContentType getType() {
234         return ContentType.UNKNOWN;
235       }
236 
237       public String getName() {
238         return _name;
239       }
240 
241       public CompoundContentImpl getParent() {
242         return CompoundContentImpl.this;
243       }
244 
245       public OleBlobImpl getBlob() {
246         return getParent().getBlob();
247       }
248 
249       public long length() {
250         return _docEntry.getSize();
251       }
252 
253       public InputStream getStream() throws IOException {
254         return new DocumentInputStream(_docEntry);
255       }
256 
257       public void writeTo(OutputStream out) throws IOException {
258         InputStream in = null;
259         try {
260           ByteUtil.copy(in = getStream(), out);
261         } finally {
262           ByteUtil.closeQuietly(in);
263         }
264       }
265 
266       @Override
267       public String toString() {
268         return CustomToStringStyle.valueBuilder(this)
269           .append("name", _name)
270           .append("length", length())
271           .toString();
272       }
273     } 
274   }
275 
276 }