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.fs; |
7 | |
8 | import java.io.IOException; |
9 | import java.io.InputStream; |
10 | import java.io.OutputStream; |
11 | import java.nio.ByteBuffer; |
12 | import java.nio.channels.FileChannel; |
13 | import java.nio.channels.FileLock; |
14 | import java.nio.channels.NonWritableChannelException; |
15 | import java.util.ArrayList; |
16 | import java.util.LinkedHashMap; |
17 | import java.util.List; |
18 | import java.util.Map; |
19 | import java.util.TreeMap; |
20 | |
21 | import org.h2.api.ErrorCode; |
22 | import org.h2.compress.CompressLZF; |
23 | import org.h2.message.DbException; |
24 | import org.h2.util.MathUtils; |
25 | import org.h2.util.New; |
26 | |
27 | /** |
28 | * This file system keeps files fully in memory. There is an option to compress |
29 | * file blocks to safe memory. |
30 | */ |
31 | public class FilePathNioMem extends FilePath { |
32 | |
33 | private static final TreeMap<String, FileNioMemData> MEMORY_FILES = |
34 | new TreeMap<String, FileNioMemData>(); |
35 | |
36 | @Override |
37 | public FilePathNioMem getPath(String path) { |
38 | FilePathNioMem p = new FilePathNioMem(); |
39 | p.name = getCanonicalPath(path); |
40 | return p; |
41 | } |
42 | |
43 | @Override |
44 | public long size() { |
45 | return getMemoryFile().length(); |
46 | } |
47 | |
48 | @Override |
49 | public void moveTo(FilePath newName, boolean atomicReplace) { |
50 | synchronized (MEMORY_FILES) { |
51 | if (!atomicReplace && !name.equals(newName.name) && |
52 | MEMORY_FILES.containsKey(newName.name)) { |
53 | throw DbException.get(ErrorCode.FILE_RENAME_FAILED_2, |
54 | new String[] { name, newName + " (exists)" }); |
55 | } |
56 | FileNioMemData f = getMemoryFile(); |
57 | f.setName(newName.name); |
58 | MEMORY_FILES.remove(name); |
59 | MEMORY_FILES.put(newName.name, f); |
60 | } |
61 | } |
62 | |
63 | @Override |
64 | public boolean createFile() { |
65 | synchronized (MEMORY_FILES) { |
66 | if (exists()) { |
67 | return false; |
68 | } |
69 | getMemoryFile(); |
70 | } |
71 | return true; |
72 | } |
73 | |
74 | @Override |
75 | public boolean exists() { |
76 | if (isRoot()) { |
77 | return true; |
78 | } |
79 | synchronized (MEMORY_FILES) { |
80 | return MEMORY_FILES.get(name) != null; |
81 | } |
82 | } |
83 | |
84 | @Override |
85 | public void delete() { |
86 | if (isRoot()) { |
87 | return; |
88 | } |
89 | synchronized (MEMORY_FILES) { |
90 | MEMORY_FILES.remove(name); |
91 | } |
92 | } |
93 | |
94 | @Override |
95 | public List<FilePath> newDirectoryStream() { |
96 | ArrayList<FilePath> list = New.arrayList(); |
97 | synchronized (MEMORY_FILES) { |
98 | for (String n : MEMORY_FILES.tailMap(name).keySet()) { |
99 | if (n.startsWith(name)) { |
100 | list.add(getPath(n)); |
101 | } else { |
102 | break; |
103 | } |
104 | } |
105 | return list; |
106 | } |
107 | } |
108 | |
109 | @Override |
110 | public boolean setReadOnly() { |
111 | return getMemoryFile().setReadOnly(); |
112 | } |
113 | |
114 | @Override |
115 | public boolean canWrite() { |
116 | return getMemoryFile().canWrite(); |
117 | } |
118 | |
119 | @Override |
120 | public FilePathNioMem getParent() { |
121 | int idx = name.lastIndexOf('/'); |
122 | return idx < 0 ? null : getPath(name.substring(0, idx)); |
123 | } |
124 | |
125 | @Override |
126 | public boolean isDirectory() { |
127 | if (isRoot()) { |
128 | return true; |
129 | } |
130 | // TODO in memory file system currently |
131 | // does not really support directories |
132 | synchronized (MEMORY_FILES) { |
133 | return MEMORY_FILES.get(name) == null; |
134 | } |
135 | } |
136 | |
137 | @Override |
138 | public boolean isAbsolute() { |
139 | // TODO relative files are not supported |
140 | return true; |
141 | } |
142 | |
143 | @Override |
144 | public FilePathNioMem toRealPath() { |
145 | return this; |
146 | } |
147 | |
148 | @Override |
149 | public long lastModified() { |
150 | return getMemoryFile().getLastModified(); |
151 | } |
152 | |
153 | @Override |
154 | public void createDirectory() { |
155 | if (exists() && isDirectory()) { |
156 | throw DbException.get(ErrorCode.FILE_CREATION_FAILED_1, |
157 | name + " (a file with this name already exists)"); |
158 | } |
159 | // TODO directories are not really supported |
160 | } |
161 | |
162 | @Override |
163 | public OutputStream newOutputStream(boolean append) throws IOException { |
164 | FileNioMemData obj = getMemoryFile(); |
165 | FileNioMem m = new FileNioMem(obj, false); |
166 | return new FileChannelOutputStream(m, append); |
167 | } |
168 | |
169 | @Override |
170 | public InputStream newInputStream() { |
171 | FileNioMemData obj = getMemoryFile(); |
172 | FileNioMem m = new FileNioMem(obj, true); |
173 | return new FileChannelInputStream(m, true); |
174 | } |
175 | |
176 | @Override |
177 | public FileChannel open(String mode) { |
178 | FileNioMemData obj = getMemoryFile(); |
179 | return new FileNioMem(obj, "r".equals(mode)); |
180 | } |
181 | |
182 | private FileNioMemData getMemoryFile() { |
183 | synchronized (MEMORY_FILES) { |
184 | FileNioMemData m = MEMORY_FILES.get(name); |
185 | if (m == null) { |
186 | m = new FileNioMemData(name, compressed()); |
187 | MEMORY_FILES.put(name, m); |
188 | } |
189 | return m; |
190 | } |
191 | } |
192 | |
193 | private boolean isRoot() { |
194 | return name.equals(getScheme()); |
195 | } |
196 | |
197 | private static String getCanonicalPath(String fileName) { |
198 | fileName = fileName.replace('\\', '/'); |
199 | int idx = fileName.indexOf(':') + 1; |
200 | if (fileName.length() > idx && fileName.charAt(idx) != '/') { |
201 | fileName = fileName.substring(0, idx) + "/" + fileName.substring(idx); |
202 | } |
203 | return fileName; |
204 | } |
205 | |
206 | @Override |
207 | public String getScheme() { |
208 | return "nioMemFS"; |
209 | } |
210 | |
211 | /** |
212 | * Whether the file should be compressed. |
213 | * |
214 | * @return if it should be compressed. |
215 | */ |
216 | boolean compressed() { |
217 | return false; |
218 | } |
219 | |
220 | } |
221 | |
222 | /** |
223 | * A memory file system that compresses blocks to conserve memory. |
224 | */ |
225 | class FilePathNioMemLZF extends FilePathNioMem { |
226 | |
227 | @Override |
228 | boolean compressed() { |
229 | return true; |
230 | } |
231 | |
232 | @Override |
233 | public String getScheme() { |
234 | return "nioMemLZF"; |
235 | } |
236 | |
237 | } |
238 | |
239 | /** |
240 | * This class represents an in-memory file. |
241 | */ |
242 | class FileNioMem extends FileBase { |
243 | |
244 | /** |
245 | * The file data. |
246 | */ |
247 | final FileNioMemData data; |
248 | |
249 | private final boolean readOnly; |
250 | private long pos; |
251 | |
252 | FileNioMem(FileNioMemData data, boolean readOnly) { |
253 | this.data = data; |
254 | this.readOnly = readOnly; |
255 | } |
256 | |
257 | @Override |
258 | public long size() { |
259 | return data.length(); |
260 | } |
261 | |
262 | @Override |
263 | public FileChannel truncate(long newLength) throws IOException { |
264 | // compatibility with JDK FileChannel#truncate |
265 | if (readOnly) { |
266 | throw new NonWritableChannelException(); |
267 | } |
268 | if (newLength < size()) { |
269 | data.touch(readOnly); |
270 | pos = Math.min(pos, newLength); |
271 | data.truncate(newLength); |
272 | } |
273 | return this; |
274 | } |
275 | |
276 | @Override |
277 | public FileChannel position(long newPos) { |
278 | this.pos = (int) newPos; |
279 | return this; |
280 | } |
281 | |
282 | @Override |
283 | public int write(ByteBuffer src) throws IOException { |
284 | int len = src.remaining(); |
285 | if (len == 0) { |
286 | return 0; |
287 | } |
288 | data.touch(readOnly); |
289 | // offset is 0 because we start writing from src.position() |
290 | pos = data.readWrite(pos, src, 0, len, true); |
291 | src.position(src.position() + len); |
292 | return len; |
293 | } |
294 | |
295 | @Override |
296 | public int read(ByteBuffer dst) throws IOException { |
297 | int len = dst.remaining(); |
298 | if (len == 0) { |
299 | return 0; |
300 | } |
301 | long newPos = data.readWrite(pos, dst, dst.position(), len, false); |
302 | len = (int) (newPos - pos); |
303 | if (len <= 0) { |
304 | return -1; |
305 | } |
306 | dst.position(dst.position() + len); |
307 | pos = newPos; |
308 | return len; |
309 | } |
310 | |
311 | @Override |
312 | public long position() { |
313 | return pos; |
314 | } |
315 | |
316 | @Override |
317 | public void implCloseChannel() throws IOException { |
318 | pos = 0; |
319 | } |
320 | |
321 | @Override |
322 | public void force(boolean metaData) throws IOException { |
323 | // do nothing |
324 | } |
325 | |
326 | @Override |
327 | public synchronized FileLock tryLock(long position, long size, |
328 | boolean shared) throws IOException { |
329 | if (shared) { |
330 | if (!data.lockShared()) { |
331 | return null; |
332 | } |
333 | } else { |
334 | if (!data.lockExclusive()) { |
335 | return null; |
336 | } |
337 | } |
338 | |
339 | // cast to FileChannel to avoid JDK 1.7 ambiguity |
340 | FileLock lock = new FileLock((FileChannel) null, position, size, shared) { |
341 | |
342 | @Override |
343 | public boolean isValid() { |
344 | return true; |
345 | } |
346 | |
347 | @Override |
348 | public void release() throws IOException { |
349 | data.unlock(); |
350 | } |
351 | }; |
352 | return lock; |
353 | } |
354 | |
355 | @Override |
356 | public String toString() { |
357 | return data.getName(); |
358 | } |
359 | |
360 | } |
361 | |
362 | /** |
363 | * This class contains the data of an in-memory random access file. |
364 | * Data compression using the LZF algorithm is supported as well. |
365 | */ |
366 | class FileNioMemData { |
367 | |
368 | private static final int CACHE_SIZE = 8; |
369 | private static final int BLOCK_SIZE_SHIFT = 10; |
370 | private static final int BLOCK_SIZE = 1 << BLOCK_SIZE_SHIFT; |
371 | private static final int BLOCK_SIZE_MASK = BLOCK_SIZE - 1; |
372 | private static final CompressLZF LZF = new CompressLZF(); |
373 | private static final byte[] BUFFER = new byte[BLOCK_SIZE * 2]; |
374 | private static final ByteBuffer COMPRESSED_EMPTY_BLOCK; |
375 | |
376 | private static final Cache<CompressItem, CompressItem> COMPRESS_LATER = |
377 | new Cache<CompressItem, CompressItem>(CACHE_SIZE); |
378 | |
379 | private String name; |
380 | private final boolean compress; |
381 | private long length; |
382 | private ByteBuffer[] data; |
383 | private long lastModified; |
384 | private boolean isReadOnly; |
385 | private boolean isLockedExclusive; |
386 | private int sharedLockCount; |
387 | |
388 | static { |
389 | byte[] n = new byte[BLOCK_SIZE]; |
390 | int len = LZF.compress(n, BLOCK_SIZE, BUFFER, 0); |
391 | COMPRESSED_EMPTY_BLOCK = ByteBuffer.allocateDirect(len); |
392 | COMPRESSED_EMPTY_BLOCK.put(BUFFER, 0, len); |
393 | } |
394 | |
395 | FileNioMemData(String name, boolean compress) { |
396 | this.name = name; |
397 | this.compress = compress; |
398 | data = new ByteBuffer[0]; |
399 | lastModified = System.currentTimeMillis(); |
400 | } |
401 | |
402 | /** |
403 | * Lock the file in exclusive mode if possible. |
404 | * |
405 | * @return if locking was successful |
406 | */ |
407 | synchronized boolean lockExclusive() { |
408 | if (sharedLockCount > 0 || isLockedExclusive) { |
409 | return false; |
410 | } |
411 | isLockedExclusive = true; |
412 | return true; |
413 | } |
414 | |
415 | /** |
416 | * Lock the file in shared mode if possible. |
417 | * |
418 | * @return if locking was successful |
419 | */ |
420 | synchronized boolean lockShared() { |
421 | if (isLockedExclusive) { |
422 | return false; |
423 | } |
424 | sharedLockCount++; |
425 | return true; |
426 | } |
427 | |
428 | /** |
429 | * Unlock the file. |
430 | */ |
431 | synchronized void unlock() { |
432 | if (isLockedExclusive) { |
433 | isLockedExclusive = false; |
434 | } else { |
435 | sharedLockCount = Math.max(0, sharedLockCount - 1); |
436 | } |
437 | } |
438 | |
439 | /** |
440 | * This small cache compresses the data if an element leaves the cache. |
441 | */ |
442 | static class Cache<K, V> extends LinkedHashMap<K, V> { |
443 | |
444 | private static final long serialVersionUID = 1L; |
445 | private final int size; |
446 | |
447 | Cache(int size) { |
448 | super(size, (float) 0.75, true); |
449 | this.size = size; |
450 | } |
451 | |
452 | @Override |
453 | protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { |
454 | if (size() < size) { |
455 | return false; |
456 | } |
457 | CompressItem c = (CompressItem) eldest.getKey(); |
458 | compress(c.data, c.page); |
459 | return true; |
460 | } |
461 | } |
462 | |
463 | /** |
464 | * Represents a compressed item. |
465 | */ |
466 | static class CompressItem { |
467 | |
468 | /** |
469 | * The file data. |
470 | */ |
471 | ByteBuffer[] data; |
472 | |
473 | /** |
474 | * The page to compress. |
475 | */ |
476 | int page; |
477 | |
478 | @Override |
479 | public int hashCode() { |
480 | return page; |
481 | } |
482 | |
483 | @Override |
484 | public boolean equals(Object o) { |
485 | if (o instanceof CompressItem) { |
486 | CompressItem c = (CompressItem) o; |
487 | return c.data == data && c.page == page; |
488 | } |
489 | return false; |
490 | } |
491 | |
492 | } |
493 | |
494 | private static void compressLater(ByteBuffer[] data, int page) { |
495 | CompressItem c = new CompressItem(); |
496 | c.data = data; |
497 | c.page = page; |
498 | synchronized (LZF) { |
499 | COMPRESS_LATER.put(c, c); |
500 | } |
501 | } |
502 | |
503 | private static void expand(ByteBuffer[] data, int page) { |
504 | ByteBuffer d = data[page]; |
505 | if (d.capacity() == BLOCK_SIZE) { |
506 | return; |
507 | } |
508 | ByteBuffer out = ByteBuffer.allocateDirect(BLOCK_SIZE); |
509 | if (d != COMPRESSED_EMPTY_BLOCK) { |
510 | synchronized (LZF) { |
511 | CompressLZF.expand(d, out); |
512 | } |
513 | } |
514 | data[page] = out; |
515 | } |
516 | |
517 | /** |
518 | * Compress the data in a byte array. |
519 | * |
520 | * @param data the page array |
521 | * @param page which page to compress |
522 | */ |
523 | static void compress(ByteBuffer[] data, int page) { |
524 | ByteBuffer d = data[page]; |
525 | synchronized (LZF) { |
526 | int len = LZF.compress(d, BUFFER, 0); |
527 | d = ByteBuffer.allocateDirect(len); |
528 | d.put(BUFFER, 0, len); |
529 | data[page] = d; |
530 | } |
531 | } |
532 | |
533 | /** |
534 | * Update the last modified time. |
535 | * |
536 | * @param openReadOnly if the file was opened in read-only mode |
537 | */ |
538 | void touch(boolean openReadOnly) throws IOException { |
539 | if (isReadOnly || openReadOnly) { |
540 | throw new IOException("Read only"); |
541 | } |
542 | lastModified = System.currentTimeMillis(); |
543 | } |
544 | |
545 | /** |
546 | * Get the file length. |
547 | * |
548 | * @return the length |
549 | */ |
550 | long length() { |
551 | return length; |
552 | } |
553 | |
554 | /** |
555 | * Truncate the file. |
556 | * |
557 | * @param newLength the new length |
558 | */ |
559 | void truncate(long newLength) { |
560 | changeLength(newLength); |
561 | long end = MathUtils.roundUpLong(newLength, BLOCK_SIZE); |
562 | if (end != newLength) { |
563 | int lastPage = (int) (newLength >>> BLOCK_SIZE_SHIFT); |
564 | expand(data, lastPage); |
565 | ByteBuffer d = data[lastPage]; |
566 | for (int i = (int) (newLength & BLOCK_SIZE_MASK); i < BLOCK_SIZE; i++) { |
567 | d.put(i, (byte) 0); |
568 | } |
569 | if (compress) { |
570 | compressLater(data, lastPage); |
571 | } |
572 | } |
573 | } |
574 | |
575 | private void changeLength(long len) { |
576 | length = len; |
577 | len = MathUtils.roundUpLong(len, BLOCK_SIZE); |
578 | int blocks = (int) (len >>> BLOCK_SIZE_SHIFT); |
579 | if (blocks != data.length) { |
580 | ByteBuffer[] n = new ByteBuffer[blocks]; |
581 | System.arraycopy(data, 0, n, 0, Math.min(data.length, n.length)); |
582 | for (int i = data.length; i < blocks; i++) { |
583 | n[i] = COMPRESSED_EMPTY_BLOCK; |
584 | } |
585 | data = n; |
586 | } |
587 | } |
588 | |
589 | /** |
590 | * Read or write. |
591 | * |
592 | * @param pos the position |
593 | * @param b the byte array |
594 | * @param off the offset within the byte array |
595 | * @param len the number of bytes |
596 | * @param write true for writing |
597 | * @return the new position |
598 | */ |
599 | long readWrite(long pos, ByteBuffer b, int off, int len, boolean write) { |
600 | long end = pos + len; |
601 | if (end > length) { |
602 | if (write) { |
603 | changeLength(end); |
604 | } else { |
605 | len = (int) (length - pos); |
606 | } |
607 | } |
608 | while (len > 0) { |
609 | int l = (int) Math.min(len, BLOCK_SIZE - (pos & BLOCK_SIZE_MASK)); |
610 | int page = (int) (pos >>> BLOCK_SIZE_SHIFT); |
611 | expand(data, page); |
612 | ByteBuffer block = data[page]; |
613 | int blockOffset = (int) (pos & BLOCK_SIZE_MASK); |
614 | if (write) { |
615 | ByteBuffer tmp = b.slice(); |
616 | tmp.position(off); |
617 | tmp.limit(off + l); |
618 | block.position(blockOffset); |
619 | block.put(tmp); |
620 | } else { |
621 | block.position(blockOffset); |
622 | ByteBuffer tmp = block.slice(); |
623 | tmp.limit(l); |
624 | int oldPosition = b.position(); |
625 | b.position(off); |
626 | b.put(tmp); |
627 | // restore old position |
628 | b.position(oldPosition); |
629 | } |
630 | if (compress) { |
631 | compressLater(data, page); |
632 | } |
633 | off += l; |
634 | pos += l; |
635 | len -= l; |
636 | } |
637 | return pos; |
638 | } |
639 | |
640 | /** |
641 | * Set the file name. |
642 | * |
643 | * @param name the name |
644 | */ |
645 | void setName(String name) { |
646 | this.name = name; |
647 | } |
648 | |
649 | /** |
650 | * Get the file name |
651 | * |
652 | * @return the name |
653 | */ |
654 | String getName() { |
655 | return name; |
656 | } |
657 | |
658 | /** |
659 | * Get the last modified time. |
660 | * |
661 | * @return the time |
662 | */ |
663 | long getLastModified() { |
664 | return lastModified; |
665 | } |
666 | |
667 | /** |
668 | * Check whether writing is allowed. |
669 | * |
670 | * @return true if it is |
671 | */ |
672 | boolean canWrite() { |
673 | return !isReadOnly; |
674 | } |
675 | |
676 | /** |
677 | * Set the read-only flag. |
678 | * |
679 | * @return true |
680 | */ |
681 | boolean setReadOnly() { |
682 | isReadOnly = true; |
683 | return true; |
684 | } |
685 | |
686 | } |
687 | |
688 | |