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.jdbc; |
7 | |
8 | import java.io.BufferedInputStream; |
9 | import java.io.BufferedOutputStream; |
10 | import java.io.ByteArrayInputStream; |
11 | import java.io.ByteArrayOutputStream; |
12 | import java.io.IOException; |
13 | import java.io.InputStream; |
14 | import java.io.OutputStream; |
15 | import java.io.PipedInputStream; |
16 | import java.io.PipedOutputStream; |
17 | import java.sql.Blob; |
18 | import java.sql.SQLException; |
19 | |
20 | import org.h2.api.ErrorCode; |
21 | import org.h2.engine.Constants; |
22 | import org.h2.message.DbException; |
23 | import org.h2.message.TraceObject; |
24 | import org.h2.util.Task; |
25 | import org.h2.util.IOUtils; |
26 | import org.h2.value.Value; |
27 | |
28 | /** |
29 | * Represents a BLOB value. |
30 | */ |
31 | public class JdbcBlob extends TraceObject implements Blob { |
32 | |
33 | Value value; |
34 | private final JdbcConnection conn; |
35 | |
36 | /** |
37 | * INTERNAL |
38 | */ |
39 | public JdbcBlob(JdbcConnection conn, Value value, int id) { |
40 | setTrace(conn.getSession().getTrace(), TraceObject.BLOB, id); |
41 | this.conn = conn; |
42 | this.value = value; |
43 | } |
44 | |
45 | /** |
46 | * Returns the length. |
47 | * |
48 | * @return the length |
49 | */ |
50 | @Override |
51 | public long length() throws SQLException { |
52 | try { |
53 | debugCodeCall("length"); |
54 | checkClosed(); |
55 | if (value.getType() == Value.BLOB) { |
56 | long precision = value.getPrecision(); |
57 | if (precision > 0) { |
58 | return precision; |
59 | } |
60 | } |
61 | return IOUtils.copyAndCloseInput(value.getInputStream(), null); |
62 | } catch (Exception e) { |
63 | throw logAndConvert(e); |
64 | } |
65 | } |
66 | |
67 | /** |
68 | * [Not supported] Truncates the object. |
69 | * |
70 | * @param len the new length |
71 | */ |
72 | @Override |
73 | public void truncate(long len) throws SQLException { |
74 | throw unsupported("LOB update"); |
75 | } |
76 | |
77 | /** |
78 | * Returns some bytes of the object. |
79 | * |
80 | * @param pos the index, the first byte is at position 1 |
81 | * @param length the number of bytes |
82 | * @return the bytes, at most length bytes |
83 | */ |
84 | @Override |
85 | public byte[] getBytes(long pos, int length) throws SQLException { |
86 | try { |
87 | if (isDebugEnabled()) { |
88 | debugCode("getBytes("+pos+", "+length+");"); |
89 | } |
90 | checkClosed(); |
91 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
92 | InputStream in = value.getInputStream(); |
93 | try { |
94 | IOUtils.skipFully(in, pos - 1); |
95 | IOUtils.copy(in, out, length); |
96 | } finally { |
97 | in.close(); |
98 | } |
99 | return out.toByteArray(); |
100 | } catch (Exception e) { |
101 | throw logAndConvert(e); |
102 | } |
103 | } |
104 | |
105 | /** |
106 | * Fills the Blob. This is only supported for new, empty Blob objects that |
107 | * were created with Connection.createBlob(). The position |
108 | * must be 1, meaning the whole Blob data is set. |
109 | * |
110 | * @param pos where to start writing (the first byte is at position 1) |
111 | * @param bytes the bytes to set |
112 | * @return the length of the added data |
113 | */ |
114 | @Override |
115 | public int setBytes(long pos, byte[] bytes) throws SQLException { |
116 | try { |
117 | if (isDebugEnabled()) { |
118 | debugCode("setBytes("+pos+", "+quoteBytes(bytes)+");"); |
119 | } |
120 | checkClosed(); |
121 | if (pos != 1) { |
122 | throw DbException.getInvalidValueException("pos", pos); |
123 | } |
124 | value = conn.createBlob(new ByteArrayInputStream(bytes), -1); |
125 | return bytes.length; |
126 | } catch (Exception e) { |
127 | throw logAndConvert(e); |
128 | } |
129 | } |
130 | |
131 | /** |
132 | * [Not supported] Sets some bytes of the object. |
133 | * |
134 | * @param pos the write position |
135 | * @param bytes the bytes to set |
136 | * @param offset the bytes offset |
137 | * @param len the number of bytes to write |
138 | * @return how many bytes have been written |
139 | */ |
140 | @Override |
141 | public int setBytes(long pos, byte[] bytes, int offset, int len) |
142 | throws SQLException { |
143 | throw unsupported("LOB update"); |
144 | } |
145 | |
146 | /** |
147 | * Returns the input stream. |
148 | * |
149 | * @return the input stream |
150 | */ |
151 | @Override |
152 | public InputStream getBinaryStream() throws SQLException { |
153 | try { |
154 | debugCodeCall("getBinaryStream"); |
155 | checkClosed(); |
156 | return value.getInputStream(); |
157 | } catch (Exception e) { |
158 | throw logAndConvert(e); |
159 | } |
160 | } |
161 | |
162 | /** |
163 | * Get a writer to update the Blob. This is only supported for new, empty |
164 | * Blob objects that were created with Connection.createBlob(). The Blob is |
165 | * created in a separate thread, and the object is only updated when |
166 | * OutputStream.close() is called. The position must be 1, meaning the whole |
167 | * Blob data is set. |
168 | * |
169 | * @param pos where to start writing (the first byte is at position 1) |
170 | * @return an output stream |
171 | */ |
172 | @Override |
173 | public OutputStream setBinaryStream(long pos) throws SQLException { |
174 | try { |
175 | if (isDebugEnabled()) { |
176 | debugCode("setBinaryStream("+pos+");"); |
177 | } |
178 | checkClosed(); |
179 | if (pos != 1) { |
180 | throw DbException.getInvalidValueException("pos", pos); |
181 | } |
182 | if (value.getPrecision() != 0) { |
183 | throw DbException.getInvalidValueException("length", value.getPrecision()); |
184 | } |
185 | final JdbcConnection c = conn; |
186 | final PipedInputStream in = new PipedInputStream(); |
187 | final Task task = new Task() { |
188 | @Override |
189 | public void call() { |
190 | value = c.createBlob(in, -1); |
191 | } |
192 | }; |
193 | PipedOutputStream out = new PipedOutputStream(in) { |
194 | @Override |
195 | public void close() throws IOException { |
196 | super.close(); |
197 | try { |
198 | task.get(); |
199 | } catch (Exception e) { |
200 | throw DbException.convertToIOException(e); |
201 | } |
202 | } |
203 | }; |
204 | task.execute(); |
205 | return new BufferedOutputStream(out); |
206 | } catch (Exception e) { |
207 | throw logAndConvert(e); |
208 | } |
209 | } |
210 | |
211 | /** |
212 | * [Not supported] Searches a pattern and return the position. |
213 | * |
214 | * @param pattern the pattern to search |
215 | * @param start the index, the first byte is at position 1 |
216 | * @return the position (first byte is at position 1), or -1 for not found |
217 | */ |
218 | @Override |
219 | public long position(byte[] pattern, long start) throws SQLException { |
220 | if (isDebugEnabled()) { |
221 | debugCode("position("+quoteBytes(pattern)+", "+start+");"); |
222 | } |
223 | if (Constants.BLOB_SEARCH) { |
224 | try { |
225 | checkClosed(); |
226 | if (pattern == null) { |
227 | return -1; |
228 | } |
229 | if (pattern.length == 0) { |
230 | return 1; |
231 | } |
232 | // TODO performance: blob pattern search is slow |
233 | BufferedInputStream in = new BufferedInputStream(value.getInputStream()); |
234 | IOUtils.skipFully(in, start - 1); |
235 | int pos = 0; |
236 | int patternPos = 0; |
237 | while (true) { |
238 | int x = in.read(); |
239 | if (x < 0) { |
240 | break; |
241 | } |
242 | if (x == (pattern[patternPos] & 0xff)) { |
243 | if (patternPos == 0) { |
244 | in.mark(pattern.length); |
245 | } |
246 | if (patternPos == pattern.length) { |
247 | return pos - patternPos; |
248 | } |
249 | patternPos++; |
250 | } else { |
251 | if (patternPos > 0) { |
252 | in.reset(); |
253 | pos -= patternPos; |
254 | } |
255 | } |
256 | pos++; |
257 | } |
258 | return -1; |
259 | } catch (Exception e) { |
260 | throw logAndConvert(e); |
261 | } |
262 | } |
263 | throw unsupported("LOB search"); |
264 | } |
265 | |
266 | /** |
267 | * [Not supported] Searches a pattern and return the position. |
268 | * |
269 | * @param blobPattern the pattern to search |
270 | * @param start the index, the first byte is at position 1 |
271 | * @return the position (first byte is at position 1), or -1 for not found |
272 | */ |
273 | @Override |
274 | public long position(Blob blobPattern, long start) throws SQLException { |
275 | if (isDebugEnabled()) { |
276 | debugCode("position(blobPattern, "+start+");"); |
277 | } |
278 | if (Constants.BLOB_SEARCH) { |
279 | try { |
280 | checkClosed(); |
281 | if (blobPattern == null) { |
282 | return -1; |
283 | } |
284 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
285 | InputStream in = blobPattern.getBinaryStream(); |
286 | while (true) { |
287 | int x = in.read(); |
288 | if (x < 0) { |
289 | break; |
290 | } |
291 | out.write(x); |
292 | } |
293 | return position(out.toByteArray(), start); |
294 | } catch (Exception e) { |
295 | throw logAndConvert(e); |
296 | } |
297 | } |
298 | throw unsupported("LOB subset"); |
299 | } |
300 | |
301 | /** |
302 | * Release all resources of this object. |
303 | */ |
304 | @Override |
305 | public void free() { |
306 | debugCodeCall("free"); |
307 | value = null; |
308 | } |
309 | |
310 | /** |
311 | * [Not supported] Returns the input stream, starting from an offset. |
312 | * |
313 | * @param pos where to start reading |
314 | * @param length the number of bytes that will be read |
315 | * @return the input stream to read |
316 | */ |
317 | @Override |
318 | public InputStream getBinaryStream(long pos, long length) throws SQLException { |
319 | throw unsupported("LOB update"); |
320 | } |
321 | |
322 | private void checkClosed() { |
323 | conn.checkClosed(); |
324 | if (value == null) { |
325 | throw DbException.get(ErrorCode.OBJECT_CLOSED); |
326 | } |
327 | } |
328 | |
329 | /** |
330 | * INTERNAL |
331 | */ |
332 | @Override |
333 | public String toString() { |
334 | return getTraceObjectName() + ": " + |
335 | (value == null ? "null" : value.getTraceSQL()); |
336 | } |
337 | |
338 | } |