--- /dev/null
+package org.apache.lucene.store;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+import java.util.Collection;
+import static java.util.Collections.synchronizedSet;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.lucene.util.ThreadInterruptedException;
+import org.apache.lucene.util.Constants;
+
+/**
+ * <a name="subclasses"/>
+ * Base class for Directory implementations that store index
+ * files in the file system. There are currently three core
+ * subclasses:
+ *
+ * <ul>
+ *
+ * <li> {@link SimpleFSDirectory} is a straightforward
+ * implementation using java.io.RandomAccessFile.
+ * However, it has poor concurrent performance
+ * (multiple threads will bottleneck) as it
+ * synchronizes when multiple threads read from the
+ * same file.
+ *
+ * <li> {@link NIOFSDirectory} uses java.nio's
+ * FileChannel's positional io when reading to avoid
+ * synchronization when reading from the same file.
+ * Unfortunately, due to a Windows-only <a
+ * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6265734">Sun
+ * JRE bug</a> this is a poor choice for Windows, but
+ * on all other platforms this is the preferred
+ * choice. Applications using {@link Thread#interrupt()} or
+ * {@link Future#cancel(boolean)} should use
+ * {@link SimpleFSDirectory} instead. See {@link NIOFSDirectory} java doc
+ * for details.
+ *
+ *
+ *
+ * <li> {@link MMapDirectory} uses memory-mapped IO when
+ * reading. This is a good choice if you have plenty
+ * of virtual memory relative to your index size, eg
+ * if you are running on a 64 bit JRE, or you are
+ * running on a 32 bit JRE but your index sizes are
+ * small enough to fit into the virtual memory space.
+ * Java has currently the limitation of not being able to
+ * unmap files from user code. The files are unmapped, when GC
+ * releases the byte buffers. Due to
+ * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4724038">
+ * this bug</a> in Sun's JRE, MMapDirectory's {@link IndexInput#close}
+ * is unable to close the underlying OS file handle. Only when
+ * GC finally collects the underlying objects, which could be
+ * quite some time later, will the file handle be closed.
+ * This will consume additional transient disk usage: on Windows,
+ * attempts to delete or overwrite the files will result in an
+ * exception; on other platforms, which typically have a "delete on
+ * last close" semantics, while such operations will succeed, the bytes
+ * are still consuming space on disk. For many applications this
+ * limitation is not a problem (e.g. if you have plenty of disk space,
+ * and you don't rely on overwriting files on Windows) but it's still
+ * an important limitation to be aware of. This class supplies a
+ * (possibly dangerous) workaround mentioned in the bug report,
+ * which may fail on non-Sun JVMs.
+ *
+ * Applications using {@link Thread#interrupt()} or
+ * {@link Future#cancel(boolean)} should use
+ * {@link SimpleFSDirectory} instead. See {@link MMapDirectory}
+ * java doc for details.
+ * </ul>
+ *
+ * Unfortunately, because of system peculiarities, there is
+ * no single overall best implementation. Therefore, we've
+ * added the {@link #open} method, to allow Lucene to choose
+ * the best FSDirectory implementation given your
+ * environment, and the known limitations of each
+ * implementation. For users who have no reason to prefer a
+ * specific implementation, it's best to simply use {@link
+ * #open}. For all others, you should instantiate the
+ * desired implementation directly.
+ *
+ * <p>The locking implementation is by default {@link
+ * NativeFSLockFactory}, but can be changed by
+ * passing in a custom {@link LockFactory} instance.
+ *
+ * @see Directory
+ */
+public abstract class FSDirectory extends Directory {
+
+ /**
+ * Default read chunk size. This is a conditional default: on 32bit JVMs, it defaults to 100 MB. On 64bit JVMs, it's
+ * <code>Integer.MAX_VALUE</code>.
+ *
+ * @see #setReadChunkSize
+ */
+ public static final int DEFAULT_READ_CHUNK_SIZE = Constants.JRE_IS_64BIT ? Integer.MAX_VALUE : 100 * 1024 * 1024;
+
+ protected final File directory; // The underlying filesystem directory
+ protected final Set<String> staleFiles = synchronizedSet(new HashSet<String>()); // Files written, but not yet sync'ed
+ private int chunkSize = DEFAULT_READ_CHUNK_SIZE; // LUCENE-1566
+
+ // returns the canonical version of the directory, creating it if it doesn't exist.
+ private static File getCanonicalPath(File file) throws IOException {
+ return new File(file.getCanonicalPath());
+ }
+
+ /** Create a new FSDirectory for the named location (ctor for subclasses).
+ * @param path the path of the directory
+ * @param lockFactory the lock factory to use, or null for the default
+ * ({@link NativeFSLockFactory});
+ * @throws IOException
+ */
+ protected FSDirectory(File path, LockFactory lockFactory) throws IOException {
+ // new ctors use always NativeFSLockFactory as default:
+ if (lockFactory == null) {
+ lockFactory = new NativeFSLockFactory();
+ }
+ directory = getCanonicalPath(path);
+
+ if (directory.exists() && !directory.isDirectory())
+ throw new NoSuchDirectoryException("file '" + directory + "' exists but is not a directory");
+
+ setLockFactory(lockFactory);
+ }
+
+ /** Creates an FSDirectory instance, trying to pick the
+ * best implementation given the current environment.
+ * The directory returned uses the {@link NativeFSLockFactory}.
+ *
+ * <p>Currently this returns {@link MMapDirectory} for most Solaris
+ * and Windows 64-bit JREs, {@link NIOFSDirectory} for other
+ * non-Windows JREs, and {@link SimpleFSDirectory} for other
+ * JREs on Windows. It is highly recommended that you consult the
+ * implementation's documentation for your platform before
+ * using this method.
+ *
+ * <p><b>NOTE</b>: this method may suddenly change which
+ * implementation is returned from release to release, in
+ * the event that higher performance defaults become
+ * possible; if the precise implementation is important to
+ * your application, please instantiate it directly,
+ * instead. For optimal performance you should consider using
+ * {@link MMapDirectory} on 64 bit JVMs.
+ *
+ * <p>See <a href="#subclasses">above</a> */
+ public static FSDirectory open(File path) throws IOException {
+ return open(path, null);
+ }
+
+ /** Just like {@link #open(File)}, but allows you to
+ * also specify a custom {@link LockFactory}. */
+ public static FSDirectory open(File path, LockFactory lockFactory) throws IOException {
+ if ((Constants.WINDOWS || Constants.SUN_OS || Constants.LINUX)
+ && Constants.JRE_IS_64BIT && MMapDirectory.UNMAP_SUPPORTED) {
+ return new MMapDirectory(path, lockFactory);
+ } else if (Constants.WINDOWS) {
+ return new SimpleFSDirectory(path, lockFactory);
+ } else {
+ return new NIOFSDirectory(path, lockFactory);
+ }
+ }
+
+ @Override
+ public void setLockFactory(LockFactory lockFactory) throws IOException {
+ super.setLockFactory(lockFactory);
+
+ // for filesystem based LockFactory, delete the lockPrefix, if the locks are placed
+ // in index dir. If no index dir is given, set ourselves
+ if (lockFactory instanceof FSLockFactory) {
+ final FSLockFactory lf = (FSLockFactory) lockFactory;
+ final File dir = lf.getLockDir();
+ // if the lock factory has no lockDir set, use the this directory as lockDir
+ if (dir == null) {
+ lf.setLockDir(directory);
+ lf.setLockPrefix(null);
+ } else if (dir.getCanonicalPath().equals(directory.getCanonicalPath())) {
+ lf.setLockPrefix(null);
+ }
+ }
+
+ }
+
+ /** Lists all files (not subdirectories) in the
+ * directory. This method never returns null (throws
+ * {@link IOException} instead).
+ *
+ * @throws NoSuchDirectoryException if the directory
+ * does not exist, or does exist but is not a
+ * directory.
+ * @throws IOException if list() returns null */
+ public static String[] listAll(File dir) throws IOException {
+ if (!dir.exists())
+ throw new NoSuchDirectoryException("directory '" + dir + "' does not exist");
+ else if (!dir.isDirectory())
+ throw new NoSuchDirectoryException("file '" + dir + "' exists but is not a directory");
+
+ // Exclude subdirs
+ String[] result = dir.list(new FilenameFilter() {
+ public boolean accept(File dir, String file) {
+ return !new File(dir, file).isDirectory();
+ }
+ });
+
+ if (result == null)
+ throw new IOException("directory '" + dir + "' exists and is a directory, but cannot be listed: list() returned null");
+
+ return result;
+ }
+
+ /** Lists all files (not subdirectories) in the
+ * directory.
+ * @see #listAll(File) */
+ @Override
+ public String[] listAll() throws IOException {
+ ensureOpen();
+ return listAll(directory);
+ }
+
+ /** Returns true iff a file with the given name exists. */
+ @Override
+ public boolean fileExists(String name) {
+ ensureOpen();
+ File file = new File(directory, name);
+ return file.exists();
+ }
+
+ /** Returns the time the named file was last modified. */
+ @Override
+ public long fileModified(String name) {
+ ensureOpen();
+ File file = new File(directory, name);
+ return file.lastModified();
+ }
+
+ /** Returns the time the named file was last modified. */
+ public static long fileModified(File directory, String name) {
+ File file = new File(directory, name);
+ return file.lastModified();
+ }
+
+ /** Set the modified time of an existing file to now.
+ * @deprecated Lucene never uses this API; it will be
+ * removed in 4.0. */
+ @Override
+ @Deprecated
+ public void touchFile(String name) {
+ ensureOpen();
+ File file = new File(directory, name);
+ file.setLastModified(System.currentTimeMillis());
+ }
+
+ /** Returns the length in bytes of a file in the directory. */
+ @Override
+ public long fileLength(String name) throws IOException {
+ ensureOpen();
+ File file = new File(directory, name);
+ final long len = file.length();
+ if (len == 0 && !file.exists()) {
+ throw new FileNotFoundException(name);
+ } else {
+ return len;
+ }
+ }
+
+ /** Removes an existing file in the directory. */
+ @Override
+ public void deleteFile(String name) throws IOException {
+ ensureOpen();
+ File file = new File(directory, name);
+ if (!file.delete())
+ throw new IOException("Cannot delete " + file);
+ staleFiles.remove(name);
+ }
+
+ /** Creates an IndexOutput for the file with the given name. */
+ @Override
+ public IndexOutput createOutput(String name) throws IOException {
+ ensureOpen();
+
+ ensureCanWrite(name);
+ return new FSIndexOutput(this, name);
+ }
+
+ protected void ensureCanWrite(String name) throws IOException {
+ if (!directory.exists())
+ if (!directory.mkdirs())
+ throw new IOException("Cannot create directory: " + directory);
+
+ File file = new File(directory, name);
+ if (file.exists() && !file.delete()) // delete existing, if any
+ throw new IOException("Cannot overwrite: " + file);
+ }
+
+ protected void onIndexOutputClosed(FSIndexOutput io) {
+ staleFiles.add(io.name);
+ }
+
+ @Deprecated
+ @Override
+ public void sync(String name) throws IOException {
+ sync(Collections.singleton(name));
+ }
+
+ @Override
+ public void sync(Collection<String> names) throws IOException {
+ ensureOpen();
+ Set<String> toSync = new HashSet<String>(names);
+ toSync.retainAll(staleFiles);
+
+ for (String name : toSync)
+ fsync(name);
+
+ staleFiles.removeAll(toSync);
+ }
+
+ // Inherit javadoc
+ @Override
+ public IndexInput openInput(String name) throws IOException {
+ ensureOpen();
+ return openInput(name, BufferedIndexInput.BUFFER_SIZE);
+ }
+
+ @Override
+ public String getLockID() {
+ ensureOpen();
+ String dirName; // name to be hashed
+ try {
+ dirName = directory.getCanonicalPath();
+ } catch (IOException e) {
+ throw new RuntimeException(e.toString(), e);
+ }
+
+ int digest = 0;
+ for(int charIDX=0;charIDX<dirName.length();charIDX++) {
+ final char ch = dirName.charAt(charIDX);
+ digest = 31 * digest + ch;
+ }
+ return "lucene-" + Integer.toHexString(digest);
+ }
+
+ /** Closes the store to future operations. */
+ @Override
+ public synchronized void close() {
+ isOpen = false;
+ }
+
+ /** @deprecated Use {@link #getDirectory} instead. */
+ @Deprecated
+ public File getFile() {
+ return getDirectory();
+ }
+
+ /** @return the underlying filesystem directory */
+ public File getDirectory() {
+ ensureOpen();
+ return directory;
+ }
+
+ /** For debug output. */
+ @Override
+ public String toString() {
+ return this.getClass().getName() + "@" + directory + " lockFactory=" + getLockFactory();
+ }
+
+ /**
+ * Sets the maximum number of bytes read at once from the
+ * underlying file during {@link IndexInput#readBytes}.
+ * The default value is {@link #DEFAULT_READ_CHUNK_SIZE};
+ *
+ * <p> This was introduced due to <a
+ * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6478546">Sun
+ * JVM Bug 6478546</a>, which throws an incorrect
+ * OutOfMemoryError when attempting to read too many bytes
+ * at once. It only happens on 32bit JVMs with a large
+ * maximum heap size.</p>
+ *
+ * <p>Changes to this value will not impact any
+ * already-opened {@link IndexInput}s. You should call
+ * this before attempting to open an index on the
+ * directory.</p>
+ *
+ * <p> <b>NOTE</b>: This value should be as large as
+ * possible to reduce any possible performance impact. If
+ * you still encounter an incorrect OutOfMemoryError,
+ * trying lowering the chunk size.</p>
+ */
+ public final void setReadChunkSize(int chunkSize) {
+ // LUCENE-1566
+ if (chunkSize <= 0) {
+ throw new IllegalArgumentException("chunkSize must be positive");
+ }
+ if (!Constants.JRE_IS_64BIT) {
+ this.chunkSize = chunkSize;
+ }
+ }
+
+ /**
+ * The maximum number of bytes to read at once from the
+ * underlying file during {@link IndexInput#readBytes}.
+ * @see #setReadChunkSize
+ */
+ public final int getReadChunkSize() {
+ // LUCENE-1566
+ return chunkSize;
+ }
+
+ protected static class FSIndexOutput extends BufferedIndexOutput {
+ private final FSDirectory parent;
+ private final String name;
+ private final RandomAccessFile file;
+ private volatile boolean isOpen; // remember if the file is open, so that we don't try to close it more than once
+
+ public FSIndexOutput(FSDirectory parent, String name) throws IOException {
+ this.parent = parent;
+ this.name = name;
+ file = new RandomAccessFile(new File(parent.directory, name), "rw");
+ isOpen = true;
+ }
+
+ /** output methods: */
+ @Override
+ public void flushBuffer(byte[] b, int offset, int size) throws IOException {
+ file.write(b, offset, size);
+ }
+
+ @Override
+ public void close() throws IOException {
+ parent.onIndexOutputClosed(this);
+ // only close the file if it has not been closed yet
+ if (isOpen) {
+ boolean success = false;
+ try {
+ super.close();
+ success = true;
+ } finally {
+ isOpen = false;
+ if (!success) {
+ try {
+ file.close();
+ } catch (Throwable t) {
+ // Suppress so we don't mask original exception
+ }
+ } else {
+ file.close();
+ }
+ }
+ }
+ }
+
+ /** Random-access methods */
+ @Override
+ public void seek(long pos) throws IOException {
+ super.seek(pos);
+ file.seek(pos);
+ }
+
+ @Override
+ public long length() throws IOException {
+ return file.length();
+ }
+
+ @Override
+ public void setLength(long length) throws IOException {
+ file.setLength(length);
+ }
+ }
+
+ protected void fsync(String name) throws IOException {
+ File fullFile = new File(directory, name);
+ boolean success = false;
+ int retryCount = 0;
+ IOException exc = null;
+ while (!success && retryCount < 5) {
+ retryCount++;
+ RandomAccessFile file = null;
+ try {
+ try {
+ file = new RandomAccessFile(fullFile, "rw");
+ file.getFD().sync();
+ success = true;
+ } finally {
+ if (file != null)
+ file.close();
+ }
+ } catch (IOException ioe) {
+ if (exc == null)
+ exc = ioe;
+ try {
+ // Pause 5 msec
+ Thread.sleep(5);
+ } catch (InterruptedException ie) {
+ throw new ThreadInterruptedException(ie);
+ }
+ }
+ }
+ if (!success)
+ // Throw original exception
+ throw exc;
+ }
+}