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.

AttachmentColumnInfo.java 15KB

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