/* * Copyright (c) 2011, 3 Round Stones Inc. Some rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the openrdf.org nor the names of its contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ package org.openrdf.store.blob.disk; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Writer; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.openrdf.store.blob.BlobObject; import org.openrdf.store.blob.BlobStore; public class DiskBlobStore implements BlobStore { private static final int MAX_HISTORY = 1000; private interface Closure<V> { V call(String name, String iri) throws IOException; }; private final File dir; final File journal; final String prefix; final AtomicLong seq = new AtomicLong(0); private final ReentrantReadWriteLock diskLock = new ReentrantReadWriteLock(); private final Map<String, Set<DiskListener>> listeners = new HashMap<String, Set<DiskListener>>(); /** version -> open DiskTransaction */ private final Map<String, WeakReference<DiskBlobVersion>> transactions; public DiskBlobStore(File dir) throws IOException { assert dir != null; this.dir = dir; this.journal = new File(dir, "$versions"); this.transactions = new WeakHashMap<String, WeakReference<DiskBlobVersion>>(); this.prefix = new File(getDirectory(), "trx").toURI().toString(); eachEntry(new Closure<Void>() { public Void call(String name, String iri) { if (iri.startsWith(prefix)) { try { String suffix = iri.substring(prefix.length()); seq.set(Math.max(seq.get(), Long.parseLong(suffix))); } catch (NumberFormatException exc) { // ignore } } return null; } }); } public String toString() { return dir.toString(); } public int hashCode() { return dir.hashCode(); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; DiskBlobStore other = (DiskBlobStore) obj; if (!dir.equals(other.dir)) return false; return true; } public BlobObject open(String uri) throws IOException { return new LiveDiskBlob(this, uri); } public DiskBlobVersion newVersion() throws IOException { return newVersion(prefix + seq.incrementAndGet()); } public DiskBlobVersion newVersion(String version) throws IOException { synchronized (transactions) { WeakReference<DiskBlobVersion> ref = transactions.get(version); if (ref != null) { DiskBlobVersion result = ref.get(); if (result != null) return result; } DiskBlobVersion result = new DiskBlobVersion(this, version, journal); ref = new WeakReference<DiskBlobVersion>(result); transactions.put(version, ref); return result; } } public DiskBlobVersion openVersion(final String version) throws IOException { File entry = eachEntry(new Closure<File>() { public File call(String name, String id) { if (id.equals(version)) return new File(journal, name); return null; } }); if (entry == null) throw new IllegalArgumentException("Unknown blob version: " + version); synchronized (transactions) { WeakReference<DiskBlobVersion> ref = transactions.get(version); if (ref != null) { DiskBlobVersion result = ref.get(); if (result != null) return result; } DiskBlobVersion result = new DiskBlobVersion(this, version, entry); ref = new WeakReference<DiskBlobVersion>(result); transactions.put(version, ref); return result; } } public String[] getRecentModifications() throws IOException { Lock readLock = readLock(); try { readLock.lock(); final LinkedList<String> history = new LinkedList<String>(); final Map<String, String> map = new HashMap<String, String>(MAX_HISTORY); eachEntry(new Closure<Void>() { public Void call(String name, String iri) { history.addFirst(name); if (history.size() > MAX_HISTORY) { history.removeLast(); map.remove(name); } map.put(name, iri); return null; } }); final LinkedList<String> blobs = new LinkedList<String>(); for (String name : history) { String version = map.get(name); File entry = new File(journal, name); new DiskBlobVersion(this, version, entry).addOpenBlobs(blobs); if (blobs.size() >= MAX_HISTORY) break; } return blobs.toArray(new String[blobs.size()]); } finally { readLock.unlock(); } } public boolean erase() throws IOException { File index = new File(journal, "index"); File tmp = new File(journal, "index$"); lock(); try { new File(journal, "obsolete").delete(); if (index.exists()) { copy(index, tmp, null); } eachEntry(tmp, new Closure<Void>() { public Void call(String name, String iri) throws IOException { openVersion(iri).erase(); return null; } }); return true; } finally { tmp.delete(); String[] list = journal.list(); if (list != null && list.length == 0) { journal.delete(); } unlock(); } } protected File getDirectory() { return dir; } protected boolean mkdirs(File dir) { if (dir.isDirectory()) return false; mkdirs(dir.getParentFile()); dir.mkdir(); dir.setReadable(false, false); dir.setReadable(true); dir.setWritable(false, false); dir.setWritable(true); dir.setExecutable(false, false); dir.setExecutable(true); return dir.isDirectory(); } protected OutputStream openOutputStream(File file) throws IOException { File dir = file.getParentFile(); mkdirs(dir); if (!dir.canWrite() || file.exists() && !file.canWrite()) throw new IOException("Cannot open blob file for writing"); file.createNewFile(); file.setReadable(false, false); file.setReadable(true); file.setWritable(false, false); file.setWritable(true); return new FileOutputStream(file); } protected Writer openWriter(File file, boolean append) throws IOException { File dir = file.getParentFile(); mkdirs(dir); if (!dir.canWrite() || file.exists() && !file.canWrite()) throw new IOException("Cannot open file for writing"); file.createNewFile(); file.setReadable(false, false); file.setReadable(true); file.setWritable(false, false); file.setWritable(true); return new FileWriter(file, append); } protected void watch(String uri, DiskListener listener) { synchronized (listeners) { Set<DiskListener> set = listeners.get(uri); if (set == null) { listeners.put(uri, set = new HashSet<DiskListener>()); } set.add(listener); } } protected boolean unwatch(String uri, DiskListener listener) { synchronized (listeners) { Set<DiskListener> set = listeners.get(uri); if (set == null) return false; boolean ret = set.remove(listener); if (set.isEmpty()) { listeners.remove(uri); } return ret; } } protected Lock readLock() { return diskLock.readLock(); } protected void lock() { diskLock.writeLock().lock(); } protected void unlock() { diskLock.writeLock().unlock(); } protected void changed(String version, Collection<String> blobs, File entry, Collection<String> previousVersions) throws IOException { Set<String> obsolete = new HashSet<String>(); for (String previous : previousVersions) { if (previous != null && this.openVersion(previous).isObsolete()) { obsolete.add(previous); } } for (String uri : blobs) { Set<DiskListener> set = listeners.get(uri); if (set != null) { for (DiskListener listener : set) { listener.changed(uri); } } } if (!obsolete.isEmpty()) { appendObsolete(obsolete); } } protected void newBlobVersion(String version, File file) throws IOException { lock(); try { File f = new File(journal, "index"); PrintWriter index = new PrintWriter(openWriter(f, true)); try { String jpath = journal.getAbsolutePath(); String path = file.getAbsolutePath(); if (path.startsWith(jpath) && path.charAt(jpath.length()) == File.separatorChar) { path = path.substring(jpath.length() + 1); } else { throw new AssertionError("Invalid version entry path: " + path); } index.print(path.replace(File.separatorChar, '/')); index.print(' '); index.println(version); } finally { index.close(); } } finally { unlock(); } } protected void removeFromIndex(String erasing) throws IOException { lock(); try { File index = new File(journal, "index"); File rest = new File(journal, "index$" + Integer.toHexString(erasing.hashCode())); boolean empty = copy(index, rest, erasing); index.delete(); if (empty) { rest.delete(); String[] list = journal.list(); if (list != null && list.length == 0) { journal.delete(); } } else { rest.renameTo(index); } } finally { unlock(); } } private boolean copy(File source, File destintation, String exclude) throws FileNotFoundException, IOException { boolean empty = true; BufferedReader reader = new BufferedReader(new FileReader(source)); try { PrintWriter writer = new PrintWriter(openWriter(destintation, false)); try { String line; while ((line = reader.readLine()) != null) { String iri = line.substring(line.indexOf(' ') + 1); if (!iri.equals(exclude)) { writer.println(line); empty = false; } } } finally { writer.close(); } } finally { reader.close(); } return empty; } private void appendObsolete(Set<String> obsolete) throws IOException { lock(); try { File f = new File(journal, "obsolete"); PrintWriter index = new PrintWriter(openWriter(f, true)); try { for (String o : obsolete) { index.println(o); } } finally { index.close(); } } finally { unlock(); } } private <V> V eachEntry(Closure<V> closure) throws IOException { return eachEntry(new File(journal, "index"), closure); } private <V> V eachEntry(File index, Closure<V> closure) throws IOException { Lock readLock = readLock(); try { readLock.lock(); if (!index.exists()) return null; BufferedReader reader = new BufferedReader(new FileReader(index)); try { String line; while ((line = reader.readLine()) != null) { String[] split = line.split("\\s+", 2); V ret = closure.call(split[0], split[1]); if (ret != null) return ret; } } finally { reader.close(); } } catch (FileNotFoundException e) { // same as empty file } finally { readLock.unlock(); } return null; } }