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