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.store; |
7 | |
8 | import java.io.BufferedInputStream; |
9 | import java.io.BufferedReader; |
10 | import java.io.IOException; |
11 | import java.io.InputStream; |
12 | import java.io.Reader; |
13 | import java.util.ArrayList; |
14 | import java.util.Arrays; |
15 | import java.util.Map.Entry; |
16 | |
17 | import org.h2.api.ErrorCode; |
18 | import org.h2.engine.Constants; |
19 | import org.h2.engine.Database; |
20 | import org.h2.message.DbException; |
21 | import org.h2.mvstore.MVMap; |
22 | import org.h2.mvstore.MVStore; |
23 | import org.h2.mvstore.StreamStore; |
24 | import org.h2.mvstore.db.MVTableEngine.Store; |
25 | import org.h2.util.IOUtils; |
26 | import org.h2.util.New; |
27 | import org.h2.value.Value; |
28 | import org.h2.value.ValueLobDb; |
29 | |
30 | /** |
31 | * This class stores LOB objects in the database, in maps. This is the back-end |
32 | * i.e. the server side of the LOB storage. |
33 | */ |
34 | public class LobStorageMap implements LobStorageInterface { |
35 | |
36 | private static final boolean TRACE = false; |
37 | |
38 | private final Database database; |
39 | |
40 | private boolean init; |
41 | |
42 | private Object nextLobIdSync = new Object(); |
43 | private long nextLobId; |
44 | |
45 | /** |
46 | * The lob metadata map. It contains the mapping from the lob id |
47 | * (which is a long) to the stream store id (which is a byte array). |
48 | * |
49 | * Key: lobId (long) |
50 | * Value: { streamStoreId (byte[]), tableId (int), |
51 | * byteCount (long), hash (long) }. |
52 | */ |
53 | private MVMap<Long, Object[]> lobMap; |
54 | |
55 | /** |
56 | * The reference map. It is used to remove data from the stream store: if no |
57 | * more entries for the given streamStoreId exist, the data is removed from |
58 | * the stream store. |
59 | * |
60 | * Key: { streamStoreId (byte[]), lobId (long) }. |
61 | * Value: true (boolean). |
62 | */ |
63 | private MVMap<Object[], Boolean> refMap; |
64 | |
65 | /** |
66 | * The stream store data map. |
67 | * |
68 | * Key: stream store block id (long). |
69 | * Value: data (byte[]). |
70 | */ |
71 | private MVMap<Long, byte[]> dataMap; |
72 | |
73 | private StreamStore streamStore; |
74 | |
75 | public LobStorageMap(Database database) { |
76 | this.database = database; |
77 | } |
78 | |
79 | @Override |
80 | public void init() { |
81 | if (init) { |
82 | return; |
83 | } |
84 | init = true; |
85 | Store s = database.getMvStore(); |
86 | MVStore mvStore; |
87 | if (s == null) { |
88 | // in-memory database |
89 | mvStore = MVStore.open(null); |
90 | } else { |
91 | mvStore = s.getStore(); |
92 | } |
93 | lobMap = mvStore.openMap("lobMap"); |
94 | refMap = mvStore.openMap("lobRef"); |
95 | dataMap = mvStore.openMap("lobData"); |
96 | streamStore = new StreamStore(dataMap); |
97 | // garbage collection of the last blocks |
98 | if (database.isReadOnly()) { |
99 | return; |
100 | } |
101 | if (dataMap.isEmpty()) { |
102 | return; |
103 | } |
104 | // search the last referenced block |
105 | // (a lob may not have any referenced blocks if data is kept inline, |
106 | // so we need to loop) |
107 | long lastUsedKey = -1; |
108 | Long lobId = lobMap.lastKey(); |
109 | while (lobId != null) { |
110 | Object[] v = lobMap.get(lobId); |
111 | byte[] id = (byte[]) v[0]; |
112 | lastUsedKey = streamStore.getMaxBlockKey(id); |
113 | if (lastUsedKey >= 0) { |
114 | break; |
115 | } |
116 | lobId = lobMap.floorKey(lobId); |
117 | } |
118 | // delete all blocks that are newer |
119 | while (true) { |
120 | Long last = dataMap.lastKey(); |
121 | if (last == null || last <= lastUsedKey) { |
122 | break; |
123 | } |
124 | dataMap.remove(last); |
125 | } |
126 | } |
127 | |
128 | @Override |
129 | public Value createBlob(InputStream in, long maxLength) { |
130 | init(); |
131 | int type = Value.BLOB; |
132 | if (maxLength < 0) { |
133 | maxLength = Long.MAX_VALUE; |
134 | } |
135 | int max = (int) Math.min(maxLength, database.getMaxLengthInplaceLob()); |
136 | try { |
137 | if (max != 0 && max < Integer.MAX_VALUE) { |
138 | BufferedInputStream b = new BufferedInputStream(in, max); |
139 | b.mark(max); |
140 | byte[] small = new byte[max]; |
141 | int len = IOUtils.readFully(b, small, max); |
142 | if (len < max) { |
143 | if (len < small.length) { |
144 | small = Arrays.copyOf(small, len); |
145 | } |
146 | return ValueLobDb.createSmallLob(type, small); |
147 | } |
148 | b.reset(); |
149 | in = b; |
150 | } |
151 | return createLob(in, type); |
152 | } catch (IllegalStateException e) { |
153 | throw DbException.get(ErrorCode.OBJECT_CLOSED, e); |
154 | } catch (IOException e) { |
155 | throw DbException.convertIOException(e, null); |
156 | } |
157 | } |
158 | |
159 | @Override |
160 | public Value createClob(Reader reader, long maxLength) { |
161 | init(); |
162 | int type = Value.CLOB; |
163 | if (maxLength < 0) { |
164 | maxLength = Long.MAX_VALUE; |
165 | } |
166 | int max = (int) Math.min(maxLength, database.getMaxLengthInplaceLob()); |
167 | try { |
168 | if (max != 0 && max < Integer.MAX_VALUE) { |
169 | BufferedReader b = new BufferedReader(reader, max); |
170 | b.mark(max); |
171 | char[] small = new char[max]; |
172 | int len = IOUtils.readFully(b, small, max); |
173 | if (len < max) { |
174 | if (len < small.length) { |
175 | small = Arrays.copyOf(small, len); |
176 | } |
177 | byte[] utf8 = new String(small, 0, len).getBytes(Constants.UTF8); |
178 | return ValueLobDb.createSmallLob(type, utf8); |
179 | } |
180 | b.reset(); |
181 | reader = b; |
182 | } |
183 | CountingReaderInputStream in = |
184 | new CountingReaderInputStream(reader, maxLength); |
185 | ValueLobDb lob = createLob(in, type); |
186 | // the length is not correct |
187 | lob = ValueLobDb.create(type, database, |
188 | lob.getTableId(), lob.getLobId(), null, in.getLength()); |
189 | return lob; |
190 | } catch (IllegalStateException e) { |
191 | throw DbException.get(ErrorCode.OBJECT_CLOSED, e); |
192 | } catch (IOException e) { |
193 | throw DbException.convertIOException(e, null); |
194 | } |
195 | } |
196 | |
197 | private ValueLobDb createLob(InputStream in, int type) throws IOException { |
198 | byte[] streamStoreId; |
199 | try { |
200 | streamStoreId = streamStore.put(in); |
201 | } catch (Exception e) { |
202 | throw DbException.convertToIOException(e); |
203 | } |
204 | long lobId = generateLobId(); |
205 | long length = streamStore.length(streamStoreId); |
206 | int tableId = LobStorageFrontend.TABLE_TEMP; |
207 | Object[] value = new Object[] { streamStoreId, tableId, length, 0 }; |
208 | lobMap.put(lobId, value); |
209 | Object[] key = new Object[] { streamStoreId, lobId }; |
210 | refMap.put(key, Boolean.TRUE); |
211 | ValueLobDb lob = ValueLobDb.create( |
212 | type, database, tableId, lobId, null, length); |
213 | if (TRACE) { |
214 | trace("create " + tableId + "/" + lobId); |
215 | } |
216 | return lob; |
217 | } |
218 | |
219 | private long generateLobId() { |
220 | synchronized (nextLobIdSync) { |
221 | if (nextLobId == 0) { |
222 | Long id = lobMap.lastKey(); |
223 | nextLobId = id == null ? 1 : id + 1; |
224 | } |
225 | return nextLobId++; |
226 | } |
227 | } |
228 | |
229 | @Override |
230 | public boolean isReadOnly() { |
231 | return database.isReadOnly(); |
232 | } |
233 | |
234 | @Override |
235 | public ValueLobDb copyLob(ValueLobDb old, int tableId, long length) { |
236 | init(); |
237 | int type = old.getType(); |
238 | long oldLobId = old.getLobId(); |
239 | long oldLength = old.getPrecision(); |
240 | if (oldLength != length) { |
241 | throw DbException.throwInternalError("Length is different"); |
242 | } |
243 | Object[] value = lobMap.get(oldLobId); |
244 | value = Arrays.copyOf(value, value.length); |
245 | byte[] streamStoreId = (byte[]) value[0]; |
246 | long lobId = generateLobId(); |
247 | value[1] = tableId; |
248 | lobMap.put(lobId, value); |
249 | Object[] key = new Object[] { streamStoreId, lobId }; |
250 | refMap.put(key, Boolean.TRUE); |
251 | ValueLobDb lob = ValueLobDb.create( |
252 | type, database, tableId, lobId, null, length); |
253 | if (TRACE) { |
254 | trace("copy " + old.getTableId() + "/" + old.getLobId() + |
255 | " > " + tableId + "/" + lobId); |
256 | } |
257 | return lob; |
258 | } |
259 | |
260 | @Override |
261 | public InputStream getInputStream(ValueLobDb lob, byte[] hmac, long byteCount) |
262 | throws IOException { |
263 | init(); |
264 | Object[] value = lobMap.get(lob.getLobId()); |
265 | if (value == null) { |
266 | throw DbException.throwInternalError("Lob not found: " + lob.getLobId()); |
267 | } |
268 | byte[] streamStoreId = (byte[]) value[0]; |
269 | return streamStore.get(streamStoreId); |
270 | } |
271 | |
272 | @Override |
273 | public void setTable(ValueLobDb lob, int tableId) { |
274 | init(); |
275 | long lobId = lob.getLobId(); |
276 | Object[] value = lobMap.remove(lobId); |
277 | if (TRACE) { |
278 | trace("move " + lob.getTableId() + "/" + lob.getLobId() + |
279 | " > " + tableId + "/" + lobId); |
280 | } |
281 | value[1] = tableId; |
282 | lobMap.put(lobId, value); |
283 | } |
284 | |
285 | @Override |
286 | public void removeAllForTable(int tableId) { |
287 | init(); |
288 | if (database.getMvStore().getStore().isClosed()) { |
289 | return; |
290 | } |
291 | // this might not be very efficient - |
292 | // to speed it up, we would need yet another map |
293 | ArrayList<Long> list = New.arrayList(); |
294 | for (Entry<Long, Object[]> e : lobMap.entrySet()) { |
295 | Object[] value = e.getValue(); |
296 | int t = (Integer) value[1]; |
297 | if (t == tableId) { |
298 | list.add(e.getKey()); |
299 | } |
300 | } |
301 | for (long lobId : list) { |
302 | removeLob(tableId, lobId); |
303 | } |
304 | if (tableId == LobStorageFrontend.TABLE_ID_SESSION_VARIABLE) { |
305 | removeAllForTable(LobStorageFrontend.TABLE_TEMP); |
306 | removeAllForTable(LobStorageFrontend.TABLE_RESULT); |
307 | } |
308 | } |
309 | |
310 | @Override |
311 | public void removeLob(ValueLobDb lob) { |
312 | init(); |
313 | int tableId = lob.getTableId(); |
314 | long lobId = lob.getLobId(); |
315 | removeLob(tableId, lobId); |
316 | } |
317 | |
318 | private void removeLob(int tableId, long lobId) { |
319 | if (TRACE) { |
320 | trace("remove " + tableId + "/" + lobId); |
321 | } |
322 | Object[] value = lobMap.remove(lobId); |
323 | if (value == null) { |
324 | // already removed |
325 | return; |
326 | } |
327 | byte[] streamStoreId = (byte[]) value[0]; |
328 | Object[] key = new Object[] {streamStoreId, lobId }; |
329 | refMap.remove(key); |
330 | // check if there are more entries for this streamStoreId |
331 | key = new Object[] {streamStoreId, 0L }; |
332 | value = refMap.ceilingKey(key); |
333 | boolean hasMoreEntries = false; |
334 | if (value != null) { |
335 | byte[] s2 = (byte[]) value[0]; |
336 | if (Arrays.equals(streamStoreId, s2)) { |
337 | hasMoreEntries = true; |
338 | } |
339 | } |
340 | if (!hasMoreEntries) { |
341 | streamStore.remove(streamStoreId); |
342 | } |
343 | } |
344 | |
345 | private static void trace(String op) { |
346 | System.out.println(Thread.currentThread().getName() + " LOB " + op); |
347 | } |
348 | |
349 | } |