/* * #! * Ontopia Content Store * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * !# */ package net.ontopia.infoset.content; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileNotFoundException; import java.io.RandomAccessFile; import java.nio.channels.FileLock; import net.ontopia.utils.StreamUtils; /** * INTERNAL: A content store implementation based on the file system. * It uses a two-level structure where inside the root directory is a * set of directories, each of which contains a fixed number of files * (N). A key is mapped to a file name (key modulo N) and a directory * name (key divided by N). The next free key is stored in a file in * the top directory, and keys are allocated in blocks. */ public class FileContentStore implements ContentStoreIF { public static final int FILES_PER_DIRECTORY = 1000; public static final int KEY_BLOCK_SIZE = 10; private int files_per_directory; private boolean open; private File store_root; private int last_key; private int end_of_key_block; private File key_file; public FileContentStore(File store_root) throws ContentStoreException { if (!store_root.canWrite()) throw new ContentStoreException("Content store root directory '" + store_root.getAbsoluteFile() + "' not writable."); // FIXME: should use java.nio.FileLock to ensure that only this // JVM accesses this directory. should also use some mechanism to // ensure that we are the only FileContentStore on this directory // within this JVM. // // UPDATE: key file is now being protected using file locks. Still // need to add locks when deleting entries. // set up members this.store_root = store_root; this.files_per_directory = FILES_PER_DIRECTORY; this.open = true; this.key_file = new File(store_root, "keyfile.txt"); // initialize allocateNewBlock(); } // --- ContentStoreIF implementation public synchronized boolean containsKey(int key) throws ContentStoreException { checkOpen(); return getFileForKey(key).exists(); } public synchronized ContentInputStream get(int key) throws ContentStoreException { checkOpen(); File file = getFileForKey(key); try { return new ContentInputStream(new FileInputStream(file), (int) file.length()); } catch (FileNotFoundException e) { throw new ContentStoreException("No entry in content store for key " + key + "; file " + file.getAbsoluteFile() + " not " + "found."); } } public int add(ContentInputStream data) throws ContentStoreException { return add(data, data.getLength()); } public synchronized int add(InputStream data, int length) throws ContentStoreException { checkOpen(); int key = getNewKey(); File file = getFileForKey(key); if (file.exists()) throw new ContentStoreException("Content store corrupted: file already " + "exists for key " + key + "."); try { // verify that container directory exists if (!file.getParentFile().exists()) file.getParentFile().mkdir(); // store data OutputStream out = new FileOutputStream(file); StreamUtils.transfer(data, out); out.close(); } catch (IOException e) { throw new ContentStoreException("Error writing data to content store.", e); } // integrity check if (file.length() != length) throw new ContentStoreException("Stored entry for key " + key + " of wrong " + "size. Given length was " + length + ", but " + "resulting entry was " + file.length()); return key; } public synchronized boolean remove(int key) throws ContentStoreException { checkOpen(); File file = getFileForKey(key); return file.delete(); } public synchronized void close() throws ContentStoreException { checkOpen(); open = false; } // --- Internal helpers private void checkOpen() throws ContentStoreException { if (!open) throw new ContentStoreException("Content store on " + store_root + "not open"); } private File getFileForKey(int key) { int dirpart = key / FILES_PER_DIRECTORY; int filepart = key % FILES_PER_DIRECTORY; return new File(store_root, "" + dirpart + File.separator + filepart); } private int getNewKey() throws ContentStoreException { if (last_key == end_of_key_block) allocateNewBlock(); last_key++; return last_key; } static final int MAX_SPINS = 1000; static final int SPIN_TIMEOUT = 10; private void allocateNewBlock() throws ContentStoreException { RandomAccessFile out = null; boolean exception_thrown = false; try { out = new RandomAccessFile(key_file, "rws"); for (int i=0; i < MAX_SPINS; i++) { // acquire exclusive lock FileLock l = out.getChannel().tryLock(); if (l == null) { // wait a little before trying again try { Thread.sleep(SPIN_TIMEOUT); } catch (InterruptedException e) { } continue; } else { try { // allocate new key int old_key; int new_key; String content = null; if (out.length() == 0) { old_key = 0; new_key = old_key + KEY_BLOCK_SIZE; } else { try { content = out.readUTF(); old_key = Integer.parseInt(content); new_key = old_key + KEY_BLOCK_SIZE; } catch (NumberFormatException e) { if (content.length() > 100) content = content.substring(0, 100) + "..."; throw new ContentStoreException("Content store key file corrupted. Contained: '" + content + "'"); } } // truncate key file and write out new key out.seek(0); out.writeUTF(Integer.toString(new_key)); end_of_key_block = new_key; last_key = old_key; return; } finally { // release file lock try { l.release(); } catch (Throwable t) { throw new ContentStoreException("Could not release key file lock.", t); } } } } throw new ContentStoreException("Block allocation timed out."); } catch (ContentStoreException e) { exception_thrown = true; throw e; } catch (Throwable t) { exception_thrown = true; throw new ContentStoreException(t); } finally { if (out != null) { try { out.close(); } catch (IOException e) { if (!exception_thrown) throw new ContentStoreException ("Problems occurred when closing content store.", e); } } } } }