1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.value; |
7 | |
8 | import java.io.BufferedInputStream; |
9 | import java.io.BufferedReader; |
10 | import java.io.ByteArrayInputStream; |
11 | import java.io.IOException; |
12 | import java.io.InputStream; |
13 | import java.io.Reader; |
14 | import java.sql.PreparedStatement; |
15 | import java.sql.SQLException; |
16 | import org.h2.engine.Constants; |
17 | import org.h2.engine.SysProperties; |
18 | import org.h2.message.DbException; |
19 | import org.h2.mvstore.DataUtils; |
20 | import org.h2.store.DataHandler; |
21 | import org.h2.store.FileStore; |
22 | import org.h2.store.FileStoreInputStream; |
23 | import org.h2.store.FileStoreOutputStream; |
24 | import org.h2.store.LobStorageFrontend; |
25 | import org.h2.store.LobStorageInterface; |
26 | import org.h2.store.fs.FileUtils; |
27 | import org.h2.util.IOUtils; |
28 | import org.h2.util.MathUtils; |
29 | import org.h2.util.StringUtils; |
30 | import org.h2.util.Utils; |
31 | |
32 | /** |
33 | * A implementation of the BLOB and CLOB data types. |
34 | * |
35 | * Small objects are kept in memory and stored in the record. |
36 | * Large objects are either stored in the database, or in temporary files. |
37 | */ |
38 | public class ValueLobDb extends Value implements Value.ValueClob, |
39 | Value.ValueBlob { |
40 | |
41 | private final int type; |
42 | private final long lobId; |
43 | private final byte[] hmac; |
44 | private final byte[] small; |
45 | private final DataHandler handler; |
46 | |
47 | /** |
48 | * For a BLOB, precision is length in bytes. |
49 | * For a CLOB, precision is length in chars. |
50 | */ |
51 | private final long precision; |
52 | |
53 | private final String fileName; |
54 | private final FileStore tempFile; |
55 | private int tableId; |
56 | private int hash; |
57 | |
58 | private ValueLobDb(int type, DataHandler handler, int tableId, long lobId, |
59 | byte[] hmac, long precision) { |
60 | this.type = type; |
61 | this.handler = handler; |
62 | this.tableId = tableId; |
63 | this.lobId = lobId; |
64 | this.hmac = hmac; |
65 | this.precision = precision; |
66 | this.small = null; |
67 | this.fileName = null; |
68 | this.tempFile = null; |
69 | } |
70 | |
71 | private ValueLobDb(int type, byte[] small, long precision) { |
72 | this.type = type; |
73 | this.small = small; |
74 | this.precision = precision; |
75 | this.lobId = 0; |
76 | this.hmac = null; |
77 | this.handler = null; |
78 | this.fileName = null; |
79 | this.tempFile = null; |
80 | } |
81 | |
82 | /** |
83 | * Create a CLOB in a temporary file. |
84 | */ |
85 | private ValueLobDb(DataHandler handler, Reader in, long remaining) |
86 | throws IOException { |
87 | this.type = Value.CLOB; |
88 | this.handler = handler; |
89 | this.small = null; |
90 | this.lobId = 0; |
91 | this.hmac = null; |
92 | this.fileName = createTempLobFileName(handler); |
93 | this.tempFile = this.handler.openFile(fileName, "rw", false); |
94 | this.tempFile.autoDelete(); |
95 | FileStoreOutputStream out = new FileStoreOutputStream(tempFile, null, null); |
96 | long tmpPrecision = 0; |
97 | try { |
98 | char[] buff = new char[Constants.IO_BUFFER_SIZE]; |
99 | while (true) { |
100 | int len = getBufferSize(this.handler, false, remaining); |
101 | len = IOUtils.readFully(in, buff, len); |
102 | if (len == 0) { |
103 | break; |
104 | } |
105 | } |
106 | } finally { |
107 | out.close(); |
108 | } |
109 | this.precision = tmpPrecision; |
110 | } |
111 | |
112 | /** |
113 | * Create a BLOB in a temporary file. |
114 | */ |
115 | private ValueLobDb(DataHandler handler, byte[] buff, int len, InputStream in, |
116 | long remaining) throws IOException { |
117 | this.type = Value.BLOB; |
118 | this.handler = handler; |
119 | this.small = null; |
120 | this.lobId = 0; |
121 | this.hmac = null; |
122 | this.fileName = createTempLobFileName(handler); |
123 | this.tempFile = this.handler.openFile(fileName, "rw", false); |
124 | this.tempFile.autoDelete(); |
125 | FileStoreOutputStream out = new FileStoreOutputStream(tempFile, null, null); |
126 | long tmpPrecision = 0; |
127 | boolean compress = this.handler.getLobCompressionAlgorithm(Value.BLOB) != null; |
128 | try { |
129 | while (true) { |
130 | tmpPrecision += len; |
131 | out.write(buff, 0, len); |
132 | remaining -= len; |
133 | if (remaining <= 0) { |
134 | break; |
135 | } |
136 | len = getBufferSize(this.handler, compress, remaining); |
137 | len = IOUtils.readFully(in, buff, len); |
138 | if (len <= 0) { |
139 | break; |
140 | } |
141 | } |
142 | } finally { |
143 | out.close(); |
144 | } |
145 | this.precision = tmpPrecision; |
146 | } |
147 | |
148 | private static String createTempLobFileName(DataHandler handler) |
149 | throws IOException { |
150 | String path = handler.getDatabasePath(); |
151 | if (path.length() == 0) { |
152 | path = SysProperties.PREFIX_TEMP_FILE; |
153 | } |
154 | return FileUtils.createTempFile(path, Constants.SUFFIX_TEMP_FILE, true, true); |
155 | } |
156 | |
157 | /** |
158 | * Create a LOB value. |
159 | * |
160 | * @param type the type |
161 | * @param handler the data handler |
162 | * @param tableId the table id |
163 | * @param id the lob id |
164 | * @param hmac the message authentication code |
165 | * @param precision the precision (number of bytes / characters) |
166 | * @return the value |
167 | */ |
168 | public static ValueLobDb create(int type, DataHandler handler, |
169 | int tableId, long id, byte[] hmac, long precision) { |
170 | return new ValueLobDb(type, handler, tableId, id, hmac, precision); |
171 | } |
172 | |
173 | /** |
174 | * Convert a lob to another data type. The data is fully read in memory |
175 | * except when converting to BLOB or CLOB. |
176 | * |
177 | * @param t the new type |
178 | * @return the converted value |
179 | */ |
180 | @Override |
181 | public Value convertTo(int t) { |
182 | if (t == type) { |
183 | return this; |
184 | } else if (t == Value.CLOB) { |
185 | if (handler != null) { |
186 | Value copy = handler.getLobStorage(). |
187 | createClob(getReader(), -1); |
188 | return copy; |
189 | } else if (small != null) { |
190 | return ValueLobDb.createSmallLob(t, small); |
191 | } |
192 | } else if (t == Value.BLOB) { |
193 | if (handler != null) { |
194 | Value copy = handler.getLobStorage(). |
195 | createBlob(getInputStream(), -1); |
196 | return copy; |
197 | } else if (small != null) { |
198 | return ValueLobDb.createSmallLob(t, small); |
199 | } |
200 | } |
201 | return super.convertTo(t); |
202 | } |
203 | |
204 | @Override |
205 | public boolean isLinked() { |
206 | return tableId != LobStorageFrontend.TABLE_ID_SESSION_VARIABLE && |
207 | small == null; |
208 | } |
209 | |
210 | public boolean isStored() { |
211 | return small == null && fileName == null; |
212 | } |
213 | |
214 | @Override |
215 | public void close() { |
216 | if (fileName != null) { |
217 | if (tempFile != null) { |
218 | tempFile.stopAutoDelete(); |
219 | } |
220 | // synchronize on the database, to avoid concurrent temp file |
221 | // creation / deletion / backup |
222 | synchronized (handler.getLobSyncObject()) { |
223 | FileUtils.delete(fileName); |
224 | } |
225 | } |
226 | if (handler != null) { |
227 | handler.getLobStorage().removeLob(this); |
228 | } |
229 | } |
230 | |
231 | @Override |
232 | public void unlink(DataHandler database) { |
233 | if (small == null && |
234 | tableId != LobStorageFrontend.TABLE_ID_SESSION_VARIABLE) { |
235 | database.getLobStorage().setTable(this, |
236 | LobStorageFrontend.TABLE_ID_SESSION_VARIABLE); |
237 | tableId = LobStorageFrontend.TABLE_ID_SESSION_VARIABLE; |
238 | } |
239 | } |
240 | |
241 | @Override |
242 | public Value link(DataHandler database, int tabId) { |
243 | if (small == null) { |
244 | if (tableId == LobStorageFrontend.TABLE_TEMP) { |
245 | database.getLobStorage().setTable(this, tabId); |
246 | this.tableId = tabId; |
247 | } else { |
248 | return handler.getLobStorage().copyLob(this, tabId, getPrecision()); |
249 | } |
250 | } else if (small.length > database.getMaxLengthInplaceLob()) { |
251 | LobStorageInterface s = database.getLobStorage(); |
252 | Value v; |
253 | if (type == Value.BLOB) { |
254 | v = s.createBlob(getInputStream(), getPrecision()); |
255 | } else { |
256 | v = s.createClob(getReader(), getPrecision()); |
257 | } |
258 | return v.link(database, tabId); |
259 | } |
260 | return this; |
261 | } |
262 | |
263 | /** |
264 | * Get the current table id of this lob. |
265 | * |
266 | * @return the table id |
267 | */ |
268 | @Override |
269 | public int getTableId() { |
270 | return tableId; |
271 | } |
272 | |
273 | @Override |
274 | public int getType() { |
275 | return type; |
276 | } |
277 | |
278 | @Override |
279 | public long getPrecision() { |
280 | return precision; |
281 | } |
282 | |
283 | @Override |
284 | public String getString() { |
285 | int len = precision > Integer.MAX_VALUE || precision == 0 ? |
286 | Integer.MAX_VALUE : (int) precision; |
287 | try { |
288 | if (type == Value.CLOB) { |
289 | if (small != null) { |
290 | return new String(small, Constants.UTF8); |
291 | } |
292 | return IOUtils.readStringAndClose(getReader(), len); |
293 | } |
294 | byte[] buff; |
295 | if (small != null) { |
296 | buff = small; |
297 | } else { |
298 | buff = IOUtils.readBytesAndClose(getInputStream(), len); |
299 | } |
300 | return StringUtils.convertBytesToHex(buff); |
301 | } catch (IOException e) { |
302 | throw DbException.convertIOException(e, toString()); |
303 | } |
304 | } |
305 | |
306 | @Override |
307 | public byte[] getBytes() { |
308 | if (type == CLOB) { |
309 | // convert hex to string |
310 | return super.getBytes(); |
311 | } |
312 | byte[] data = getBytesNoCopy(); |
313 | return Utils.cloneByteArray(data); |
314 | } |
315 | |
316 | @Override |
317 | public byte[] getBytesNoCopy() { |
318 | if (type == CLOB) { |
319 | // convert hex to string |
320 | return super.getBytesNoCopy(); |
321 | } |
322 | if (small != null) { |
323 | return small; |
324 | } |
325 | try { |
326 | return IOUtils.readBytesAndClose(getInputStream(), Integer.MAX_VALUE); |
327 | } catch (IOException e) { |
328 | throw DbException.convertIOException(e, toString()); |
329 | } |
330 | } |
331 | |
332 | @Override |
333 | public int hashCode() { |
334 | if (hash == 0) { |
335 | if (precision > 4096) { |
336 | // TODO: should calculate the hash code when saving, and store |
337 | // it in the database file |
338 | return (int) (precision ^ (precision >>> 32)); |
339 | } |
340 | if (type == CLOB) { |
341 | hash = getString().hashCode(); |
342 | } else { |
343 | hash = Utils.getByteArrayHash(getBytes()); |
344 | } |
345 | } |
346 | return hash; |
347 | } |
348 | |
349 | @Override |
350 | protected int compareSecure(Value v, CompareMode mode) { |
351 | if (v instanceof ValueLobDb) { |
352 | ValueLobDb v2 = (ValueLobDb) v; |
353 | if (v == this) { |
354 | return 0; |
355 | } |
356 | if (lobId == v2.lobId && small == null && v2.small == null) { |
357 | return 0; |
358 | } |
359 | } |
360 | if (type == Value.CLOB) { |
361 | return Integer.signum(getString().compareTo(v.getString())); |
362 | } |
363 | byte[] v2 = v.getBytesNoCopy(); |
364 | return Utils.compareNotNullSigned(getBytes(), v2); |
365 | } |
366 | |
367 | @Override |
368 | public Object getObject() { |
369 | if (type == Value.CLOB) { |
370 | return getReader(); |
371 | } |
372 | return getInputStream(); |
373 | } |
374 | |
375 | @Override |
376 | public Reader getReader() { |
377 | return IOUtils.getBufferedReader(getInputStream()); |
378 | } |
379 | |
380 | @Override |
381 | public InputStream getInputStream() { |
382 | if (small != null) { |
383 | return new ByteArrayInputStream(small); |
384 | } else if (fileName != null) { |
385 | FileStore store = handler.openFile(fileName, "r", true); |
386 | boolean alwaysClose = SysProperties.lobCloseBetweenReads; |
387 | return new BufferedInputStream(new FileStoreInputStream(store, |
388 | handler, false, alwaysClose), Constants.IO_BUFFER_SIZE); |
389 | } |
390 | long byteCount = (type == Value.BLOB) ? precision : -1; |
391 | try { |
392 | return handler.getLobStorage().getInputStream(this, hmac, byteCount); |
393 | } catch (IOException e) { |
394 | throw DbException.convertIOException(e, toString()); |
395 | } |
396 | } |
397 | |
398 | @Override |
399 | public void set(PreparedStatement prep, int parameterIndex) |
400 | throws SQLException { |
401 | long p = getPrecision(); |
402 | if (p > Integer.MAX_VALUE || p <= 0) { |
403 | p = -1; |
404 | } |
405 | if (type == Value.BLOB) { |
406 | prep.setBinaryStream(parameterIndex, getInputStream(), (int) p); |
407 | } else { |
408 | prep.setCharacterStream(parameterIndex, getReader(), (int) p); |
409 | } |
410 | } |
411 | |
412 | @Override |
413 | public String getSQL() { |
414 | String s; |
415 | if (type == Value.CLOB) { |
416 | s = getString(); |
417 | return StringUtils.quoteStringSQL(s); |
418 | } |
419 | byte[] buff = getBytes(); |
420 | s = StringUtils.convertBytesToHex(buff); |
421 | return "X'" + s + "'"; |
422 | } |
423 | |
424 | @Override |
425 | public String getTraceSQL() { |
426 | if (small != null && getPrecision() <= SysProperties.MAX_TRACE_DATA_LENGTH) { |
427 | return getSQL(); |
428 | } |
429 | StringBuilder buff = new StringBuilder(); |
430 | if (type == Value.CLOB) { |
431 | buff.append("SPACE(").append(getPrecision()); |
432 | } else { |
433 | buff.append("CAST(REPEAT('00', ").append(getPrecision()).append(") AS BINARY"); |
434 | } |
435 | buff.append(" /* table: ").append(tableId).append(" id: ") |
436 | .append(lobId).append(" */)"); |
437 | return buff.toString(); |
438 | } |
439 | |
440 | /** |
441 | * Get the data if this a small lob value. |
442 | * |
443 | * @return the data |
444 | */ |
445 | @Override |
446 | public byte[] getSmall() { |
447 | return small; |
448 | } |
449 | |
450 | @Override |
451 | public int getDisplaySize() { |
452 | return MathUtils.convertLongToInt(getPrecision()); |
453 | } |
454 | |
455 | @Override |
456 | public boolean equals(Object other) { |
457 | return other instanceof ValueLobDb && compareSecure((Value) other, null) == 0; |
458 | } |
459 | |
460 | @Override |
461 | public int getMemory() { |
462 | if (small != null) { |
463 | return small.length + 104; |
464 | } |
465 | return 140; |
466 | } |
467 | |
468 | /** |
469 | * Create an independent copy of this temporary value. |
470 | * The file will not be deleted automatically. |
471 | * |
472 | * @return the value |
473 | */ |
474 | @Override |
475 | public ValueLobDb copyToTemp() { |
476 | return this; |
477 | } |
478 | |
479 | /** |
480 | * Create an independent copy of this value, |
481 | * that will be bound to a result. |
482 | * |
483 | * @return the value (this for small objects) |
484 | */ |
485 | @Override |
486 | public ValueLobDb copyToResult() { |
487 | if (handler == null) { |
488 | return this; |
489 | } |
490 | LobStorageInterface s = handler.getLobStorage(); |
491 | if (s.isReadOnly()) { |
492 | return this; |
493 | } |
494 | return s.copyLob(this, LobStorageFrontend.TABLE_RESULT, |
495 | getPrecision()); |
496 | } |
497 | |
498 | public long getLobId() { |
499 | return lobId; |
500 | } |
501 | |
502 | @Override |
503 | public String toString() { |
504 | return "lob: " + fileName + " table: " + tableId + " id: " + lobId; |
505 | } |
506 | |
507 | /** |
508 | * Create a temporary CLOB value from a stream. |
509 | * |
510 | * @param in the reader |
511 | * @param length the number of characters to read, or -1 for no limit |
512 | * @param handler the data handler |
513 | * @return the lob value |
514 | */ |
515 | public static ValueLobDb createTempClob(Reader in, long length, |
516 | DataHandler handler) { |
517 | BufferedReader reader; |
518 | if (in instanceof BufferedReader) { |
519 | reader = (BufferedReader) in; |
520 | } else { |
521 | reader = new BufferedReader(in, Constants.IO_BUFFER_SIZE); |
522 | } |
523 | try { |
524 | boolean compress = handler.getLobCompressionAlgorithm(Value.CLOB) != null; |
525 | long remaining = Long.MAX_VALUE; |
526 | if (length >= 0 && length < remaining) { |
527 | remaining = length; |
528 | } |
529 | int len = getBufferSize(handler, compress, remaining); |
530 | char[] buff; |
531 | if (len >= Integer.MAX_VALUE) { |
532 | String data = IOUtils.readStringAndClose(reader, -1); |
533 | buff = data.toCharArray(); |
534 | len = buff.length; |
535 | } else { |
536 | buff = new char[len]; |
537 | reader.mark(len); |
538 | len = IOUtils.readFully(reader, buff, len); |
539 | } |
540 | if (len <= handler.getMaxLengthInplaceLob()) { |
541 | byte[] small = new String(buff, 0, len).getBytes(Constants.UTF8); |
542 | return ValueLobDb.createSmallLob(Value.CLOB, small, len); |
543 | } |
544 | reader.reset(); |
545 | ValueLobDb lob = new ValueLobDb(handler, reader, remaining); |
546 | return lob; |
547 | } catch (IOException e) { |
548 | throw DbException.convertIOException(e, null); |
549 | } |
550 | } |
551 | |
552 | /** |
553 | * Create a temporary BLOB value from a stream. |
554 | * |
555 | * @param in the input stream |
556 | * @param length the number of characters to read, or -1 for no limit |
557 | * @param handler the data handler |
558 | * @return the lob value |
559 | */ |
560 | public static ValueLobDb createTempBlob(InputStream in, long length, |
561 | DataHandler handler) { |
562 | try { |
563 | long remaining = Long.MAX_VALUE; |
564 | boolean compress = handler.getLobCompressionAlgorithm(Value.BLOB) != null; |
565 | if (length >= 0 && length < remaining) { |
566 | remaining = length; |
567 | } |
568 | int len = getBufferSize(handler, compress, remaining); |
569 | byte[] buff; |
570 | if (len >= Integer.MAX_VALUE) { |
571 | buff = IOUtils.readBytesAndClose(in, -1); |
572 | len = buff.length; |
573 | } else { |
574 | buff = DataUtils.newBytes(len); |
575 | len = IOUtils.readFully(in, buff, len); |
576 | } |
577 | if (len <= handler.getMaxLengthInplaceLob()) { |
578 | byte[] small = DataUtils.newBytes(len); |
579 | System.arraycopy(buff, 0, small, 0, len); |
580 | return ValueLobDb.createSmallLob(Value.BLOB, small, small.length); |
581 | } |
582 | ValueLobDb lob = new ValueLobDb(handler, buff, len, in, remaining); |
583 | return lob; |
584 | } catch (IOException e) { |
585 | throw DbException.convertIOException(e, null); |
586 | } |
587 | } |
588 | |
589 | private static int getBufferSize(DataHandler handler, boolean compress, |
590 | long remaining) { |
591 | if (remaining < 0 || remaining > Integer.MAX_VALUE) { |
592 | remaining = Integer.MAX_VALUE; |
593 | } |
594 | int inplace = handler.getMaxLengthInplaceLob(); |
595 | long m = compress ? Constants.IO_BUFFER_SIZE_COMPRESS |
596 | : Constants.IO_BUFFER_SIZE; |
597 | if (m < remaining && m <= inplace) { |
598 | // using "1L" to force long arithmetic because |
599 | // inplace could be Integer.MAX_VALUE |
600 | m = Math.min(remaining, inplace + 1L); |
601 | // the buffer size must be bigger than the inplace lob, otherwise we |
602 | // can't know if it must be stored in-place or not |
603 | m = MathUtils.roundUpLong(m, Constants.IO_BUFFER_SIZE); |
604 | } |
605 | m = Math.min(remaining, m); |
606 | m = MathUtils.convertLongToInt(m); |
607 | if (m < 0) { |
608 | m = Integer.MAX_VALUE; |
609 | } |
610 | return (int) m; |
611 | } |
612 | |
613 | @Override |
614 | public Value convertPrecision(long precision, boolean force) { |
615 | if (this.precision <= precision) { |
616 | return this; |
617 | } |
618 | ValueLobDb lob; |
619 | if (type == CLOB) { |
620 | if (handler == null) { |
621 | try { |
622 | int p = MathUtils.convertLongToInt(precision); |
623 | String s = IOUtils.readStringAndClose(getReader(), p); |
624 | byte[] data = s.getBytes(Constants.UTF8); |
625 | lob = ValueLobDb.createSmallLob(type, data, s.length()); |
626 | } catch (IOException e) { |
627 | throw DbException.convertIOException(e, null); |
628 | } |
629 | } else { |
630 | lob = ValueLobDb.createTempClob(getReader(), precision, handler); |
631 | } |
632 | } else { |
633 | if (handler == null) { |
634 | try { |
635 | int p = MathUtils.convertLongToInt(precision); |
636 | byte[] data = IOUtils.readBytesAndClose(getInputStream(), p); |
637 | lob = ValueLobDb.createSmallLob(type, data, data.length); |
638 | } catch (IOException e) { |
639 | throw DbException.convertIOException(e, null); |
640 | } |
641 | } else { |
642 | lob = ValueLobDb.createTempBlob(getInputStream(), precision, handler); |
643 | } |
644 | } |
645 | return lob; |
646 | } |
647 | |
648 | /** |
649 | * Create a LOB object that fits in memory. |
650 | * |
651 | * @param type the type (Value.BLOB or CLOB) |
652 | * @param small the byte array |
653 | * @return the LOB |
654 | */ |
655 | public static Value createSmallLob(int type, byte[] small) { |
656 | int precision; |
657 | if (type == Value.CLOB) { |
658 | precision = new String(small, Constants.UTF8).length(); |
659 | } else { |
660 | precision = small.length; |
661 | } |
662 | return createSmallLob(type, small, precision); |
663 | } |
664 | |
665 | /** |
666 | * Create a LOB object that fits in memory. |
667 | * |
668 | * @param type the type (Value.BLOB or CLOB) |
669 | * @param small the byte array |
670 | * @param precision the precision |
671 | * @return the LOB |
672 | */ |
673 | public static ValueLobDb createSmallLob(int type, byte[] small, |
674 | long precision) { |
675 | return new ValueLobDb(type, small, precision); |
676 | } |
677 | |
678 | } |