View Javadoc
1   /*
2   Copyright (c) 2011 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.complex;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.DataInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.nio.ByteBuffer;
25  import java.util.Arrays;
26  import java.util.Date;
27  import java.util.HashSet;
28  import java.util.Set;
29  import java.util.zip.Deflater;
30  import java.util.zip.DeflaterOutputStream;
31  import java.util.zip.InflaterInputStream;
32  
33  import com.healthmarketscience.jackcess.Column;
34  import com.healthmarketscience.jackcess.Row;
35  import com.healthmarketscience.jackcess.Table;
36  import com.healthmarketscience.jackcess.complex.Attachment;
37  import com.healthmarketscience.jackcess.complex.AttachmentColumnInfo;
38  import com.healthmarketscience.jackcess.complex.ComplexDataType;
39  import com.healthmarketscience.jackcess.complex.ComplexValue;
40  import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey;
41  import com.healthmarketscience.jackcess.impl.ByteUtil;
42  import com.healthmarketscience.jackcess.impl.ColumnImpl;
43  import com.healthmarketscience.jackcess.impl.JetFormat;
44  import com.healthmarketscience.jackcess.impl.PageChannel;
45  
46  
47  /**
48   * Complex column info for a column holding 0 or more attachments per row.
49   *
50   * @author James Ahlborn
51   */
52  public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment>
53    implements AttachmentColumnInfo
54  {
55    /** some file formats which may not be worth re-compressing */
56    private static final Set<String> COMPRESSED_FORMATS = new HashSet<String>(
57        Arrays.asList("jpg", "zip", "gz", "bz2", "z", "7z", "cab", "rar", 
58                      "mp3", "mpg"));
59  
60    private static final String FILE_NAME_COL_NAME = "FileName";
61    private static final String FILE_TYPE_COL_NAME = "FileType";
62  
63    private static final int DATA_TYPE_RAW = 0;
64    private static final int DATA_TYPE_COMPRESSED = 1;
65  
66    private static final int UNKNOWN_HEADER_VAL = 1;
67    private static final int WRAPPER_HEADER_SIZE = 8;
68    private static final int CONTENT_HEADER_SIZE = 12;
69  
70    private final Column _fileUrlCol;
71    private final Column _fileNameCol;
72    private final Column _fileTypeCol;
73    private final Column _fileDataCol;
74    private final Column _fileTimeStampCol;
75    private final Column _fileFlagsCol;
76    
77    public AttachmentColumnInfoImpl(Column column, int complexId,
78                                    Table typeObjTable, Table flatTable)
79      throws IOException
80    {
81      super(column, complexId, typeObjTable, flatTable);
82  
83      Column fileUrlCol = null;
84      Column fileNameCol = null;
85      Column fileTypeCol = null;
86      Column fileDataCol = null;
87      Column fileTimeStampCol = null;
88      Column fileFlagsCol = null;
89  
90      for(Column col : getTypeColumns()) {
91        switch(col.getType()) {
92        case TEXT:
93          if(FILE_NAME_COL_NAME.equalsIgnoreCase(col.getName())) {
94            fileNameCol = col;
95          } else if(FILE_TYPE_COL_NAME.equalsIgnoreCase(col.getName())) {
96            fileTypeCol = col;
97          } else {
98            // if names don't match, assign in order: name, type
99            if(fileNameCol == null) {
100             fileNameCol = col;
101           } else if(fileTypeCol == null) {
102             fileTypeCol = col;
103           }
104         }
105         break;
106       case LONG:
107         fileFlagsCol = col;
108         break;
109       case SHORT_DATE_TIME:
110         fileTimeStampCol = col;
111         break;
112       case OLE:
113         fileDataCol = col;
114         break;
115       case MEMO:
116         fileUrlCol = col;
117         break;
118       default:
119         // ignore
120       }
121     }
122     
123     _fileUrlCol = fileUrlCol;
124     _fileNameCol = fileNameCol;
125     _fileTypeCol = fileTypeCol;
126     _fileDataCol = fileDataCol;
127     _fileTimeStampCol = fileTimeStampCol;
128     _fileFlagsCol = fileFlagsCol;
129   }
130 
131   public Column getFileUrlColumn() {
132     return _fileUrlCol;
133   }
134   
135   public Column getFileNameColumn() {
136     return _fileNameCol;
137   }
138 
139   public Column getFileTypeColumn() {
140     return _fileTypeCol;
141   }
142   
143   public Column getFileDataColumn() {
144     return _fileDataCol;
145   }
146   
147   public Column getFileTimeStampColumn() {
148     return _fileTimeStampCol;
149   }
150   
151   public Column getFileFlagsColumn() {
152     return _fileFlagsCol;
153   }  
154   
155   @Override
156   public ComplexDataType getType()
157   {
158     return ComplexDataType.ATTACHMENT;
159   }
160 
161   @Override
162   protected AttachmentImpl toValue(ComplexValueForeignKey complexValueFk,
163                                    Row rawValue) {
164     ComplexValue.Id id = getValueId(rawValue);
165     String url = (String)getFileUrlColumn().getRowValue(rawValue);
166     String name = (String)getFileNameColumn().getRowValue(rawValue);
167     String type = (String)getFileTypeColumn().getRowValue(rawValue);
168     Integer flags = (Integer)getFileFlagsColumn().getRowValue(rawValue);
169     Date ts = (Date)getFileTimeStampColumn().getRowValue(rawValue);
170     byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue);
171     
172     return new AttachmentImpl(id, complexValueFk, url, name, type, null,
173                               ts, flags, data);
174   }
175 
176   @Override
177   protected Object[] asRow(Object[] row, Attachment attachment) 
178     throws IOException 
179   {
180     super.asRow(row, attachment);
181     getFileUrlColumn().setRowValue(row, attachment.getFileUrl());
182     getFileNameColumn().setRowValue(row, attachment.getFileName());
183     getFileTypeColumn().setRowValue(row, attachment.getFileType());
184     getFileFlagsColumn().setRowValue(row, attachment.getFileFlags());
185     getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStamp());
186     getFileDataColumn().setRowValue(row, attachment.getEncodedFileData());
187     return row;
188   }
189 
190   public static Attachment newAttachment(byte[] data) {
191     return newAttachment(INVALID_FK, data);
192   }
193   
194   public static Attachment newAttachment(ComplexValueForeignKey complexValueFk,
195                                          byte[] data) {
196     return newAttachment(complexValueFk, null, null, null, data, null, null);
197   }
198 
199   public static Attachment newAttachment(
200       String url, String name, String type, byte[] data,
201       Date timeStamp, Integer flags)
202   {
203     return newAttachment(INVALID_FK, url, name, type, data,
204                          timeStamp, flags);
205   }
206   
207   public static Attachment newAttachment(
208       ComplexValueForeignKey complexValueFk, String url, String name,
209       String type, byte[] data, Date timeStamp, Integer flags)
210   {
211     return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type,
212                               data, timeStamp, flags, null);
213   }
214 
215   public static Attachment newEncodedAttachment(byte[] encodedData) {
216     return newEncodedAttachment(INVALID_FK, encodedData);
217   }
218     
219   public static Attachment newEncodedAttachment(
220       ComplexValueForeignKey complexValueFk, byte[] encodedData) {
221     return newEncodedAttachment(complexValueFk, null, null, null, encodedData,
222                                 null, null);
223   }
224  
225   public static Attachment newEncodedAttachment(
226       String url, String name, String type, byte[] encodedData,
227       Date timeStamp, Integer flags)
228   {
229     return newEncodedAttachment(INVALID_FK, url, name, type, 
230                                 encodedData, timeStamp, flags);
231   }
232    
233   public static Attachment newEncodedAttachment(
234       ComplexValueForeignKey complexValueFk, String url, String name,
235       String type, byte[] encodedData, Date timeStamp, Integer flags)
236   {
237     return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type,
238                               null, timeStamp, flags, encodedData);
239   } 
240 
241   
242   private static class AttachmentImpl extends ComplexValueImpl
243     implements Attachment
244   {
245     private String _url;
246     private String _name;
247     private String _type;
248     private byte[] _data;
249     private Date _timeStamp;
250     private Integer _flags;
251     private byte[] _encodedData;
252 
253     private AttachmentImpl(Id id, ComplexValueForeignKey complexValueFk,
254                            String url, String name, String type, byte[] data,
255                            Date timeStamp, Integer flags, byte[] encodedData)
256     {
257       super(id, complexValueFk);
258       _url = url;
259       _name = name;
260       _type = type;
261       _data = data;
262       _timeStamp = timeStamp;
263       _flags = flags;
264       _encodedData = encodedData;
265     }
266     
267     public byte[] getFileData() throws IOException {
268       if((_data == null) && (_encodedData != null)) {
269         _data = decodeData();
270       }
271       return _data;
272     }
273 
274     public void setFileData(byte[] data) {
275       _data = data;
276       _encodedData = null;
277     }
278 
279     public byte[] getEncodedFileData() throws IOException {
280       if((_encodedData == null) && (_data != null)) {
281         _encodedData = encodeData();
282       }
283       return _encodedData;
284     }
285 
286     public void setEncodedFileData(byte[] data) {
287       _encodedData = data;
288       _data = null;
289     }
290 
291     public String getFileName() {
292       return _name;
293     }
294 
295     public void setFileName(String fileName) {
296       _name = fileName;
297     }
298   
299     public String getFileUrl() {
300       return _url;
301     }
302 
303     public void setFileUrl(String fileUrl) {
304       _url = fileUrl;
305     }
306   
307     public String getFileType() {
308       return _type;
309     }
310 
311     public void setFileType(String fileType) {
312       _type = fileType;
313     }
314   
315     public Date getFileTimeStamp() {
316       return _timeStamp;
317     }
318 
319     public void setFileTimeStamp(Date fileTimeStamp) {
320       _timeStamp = fileTimeStamp;
321     }
322   
323     public Integer getFileFlags() {
324       return _flags;
325     }
326 
327     public void setFileFlags(Integer fileFlags) {
328       _flags = fileFlags;
329     }  
330 
331     public void update() throws IOException {
332       getComplexValueForeignKey().updateAttachment(this);
333     }
334     
335     public void delete() throws IOException {
336       getComplexValueForeignKey().deleteAttachment(this);
337     }
338     
339     @Override
340     public String toString() {
341 
342       String dataStr = null;
343       try {
344         dataStr = ByteUtil.toHexString(getFileData());
345       } catch(IOException e) {
346         dataStr = e.toString();
347       }
348       
349       return "Attachment(" + getComplexValueForeignKey() + "," + getId() +
350         ") " + getFileUrl() + ", " + getFileName() + ", " + getFileType()
351         + ", " + getFileTimeStamp() + ", " + getFileFlags()  + ", " +
352         dataStr;
353     } 
354 
355     /**
356      * Decodes the raw attachment file data to get the _actual_ content.
357      */
358     private byte[] decodeData() throws IOException {
359 
360       if(_encodedData.length < WRAPPER_HEADER_SIZE) {
361         // nothing we can do
362         throw new IOException("Unknown encoded attachment data format");
363       }
364   
365       // read initial header info
366       ByteBuffer bb = PageChannel.wrap(_encodedData);
367       int typeFlag = bb.getInt();
368       int dataLen = bb.getInt();
369 
370       DataInputStream contentStream = null;
371       try {
372         InputStream bin = new ByteArrayInputStream(
373             _encodedData, WRAPPER_HEADER_SIZE,
374             _encodedData.length - WRAPPER_HEADER_SIZE);
375 
376         if(typeFlag == DATA_TYPE_RAW) {
377           // nothing else to do
378         } else if(typeFlag == DATA_TYPE_COMPRESSED) {
379           // actual content is deflate compressed
380           bin = new InflaterInputStream(bin);
381         } else {
382           throw new IOException(
383               "Unknown encoded attachment data type " + typeFlag);
384         }
385 
386         contentStream = new DataInputStream(bin);
387 
388         // header is an unknown flag followed by the "file extension" of the
389         // data (no clue why we need that again since it's already a separate
390         // field in the attachment table).  just skip all of it
391         byte[] tmpBytes = new byte[4];
392         contentStream.readFully(tmpBytes);
393         int headerLen = PageChannel.wrap(tmpBytes).getInt();
394         contentStream.skipBytes(headerLen - 4);
395 
396         // calculate actual data length and read it (note, header length
397         // includes the bytes for the length)
398         tmpBytes = new byte[dataLen - headerLen];
399         contentStream.readFully(tmpBytes);
400 
401         return tmpBytes;
402 
403       } finally {
404         ByteUtil.closeQuietly(contentStream);
405       }
406     }
407 
408     /**
409      * Encodes the actual attachment file data to get the raw, stored format.
410      */
411     private byte[] encodeData() throws IOException {
412 
413       // possibly compress data based on file type
414       String type = ((_type != null) ? _type.toLowerCase() : "");
415       boolean shouldCompress = !COMPRESSED_FORMATS.contains(type);
416 
417       // encode extension, which ends w/ a null byte
418       type += '\0';
419       ByteBuffer typeBytes = ColumnImpl.encodeUncompressedText(
420           type, JetFormat.VERSION_12.CHARSET);
421       int headerLen = typeBytes.remaining() + CONTENT_HEADER_SIZE;
422 
423       int dataLen = _data.length;
424       ByteUtil.ByteStream dataStream = new ByteUtil.ByteStream(
425           WRAPPER_HEADER_SIZE + headerLen + dataLen);
426 
427       // write the wrapper header info
428       ByteBuffer bb = PageChannel.wrap(dataStream.getBytes());
429       bb.putInt(shouldCompress ? DATA_TYPE_COMPRESSED : DATA_TYPE_RAW);
430       bb.putInt(dataLen + headerLen);
431       dataStream.skip(WRAPPER_HEADER_SIZE);
432 
433       OutputStream contentStream = dataStream;
434       Deflater deflater = null;
435       try {
436 
437         if(shouldCompress) {
438           contentStream = new DeflaterOutputStream(
439               contentStream, deflater = new Deflater(3));
440         }
441 
442         // write the header w/ the file extension
443         byte[] tmpBytes = new byte[CONTENT_HEADER_SIZE];
444         PageChannel.wrap(tmpBytes)
445           .putInt(headerLen)
446           .putInt(UNKNOWN_HEADER_VAL)
447           .putInt(type.length());
448         contentStream.write(tmpBytes);
449         contentStream.write(typeBytes.array(), 0, typeBytes.remaining());
450 
451         // write the _actual_ contents
452         contentStream.write(_data);
453         contentStream.close();
454         contentStream = null;
455 
456         return dataStream.toByteArray();
457 
458       } finally {
459         ByteUtil.closeQuietly(contentStream);
460         if(deflater != null) {
461           deflater.end();
462         }
463       }
464     }
465   }
466 
467 }