/*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* The Original Code is the Kowari Metadata Store.
*
* The Initial Developer of the Original Code is Plugged In Software Pty
* Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions
* created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002
* Plugged In Software Pty Ltd. All Rights Reserved.
*
* Contributor(s): N/A.
*
* [NOTE: The text of this Exhibit A may differ slightly from the text
* of the notices in the Source Code files of the Original Code. You
* should use the text of this Exhibit A rather than the text found in the
* Original Code Source Code for Your Modifications.]
*
*/
package org.mulgara.store.xa;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
// Java 2 standard packages
import java.util.*;
// Third party packages
import org.apache.log4j.Logger;
import org.mulgara.util.io.MappingUtil;
/**
* An abstract class that represents a file which consists of a number of
* blocks that are all the same size. <p>
*
* The implementations are only thread-safe if there is no more than one thread
* writing to the file at any given time and if the writing thread does not
* write to blocks being read by the reading threads and does not make the file
* smaller than the size required by the reading threads.
*
* @created 2001-09-20
*
* @author David Makepeace
*
* @version $Revision: 1.10 $
*
* @modified $Date: 2005/06/30 01:14:39 $ @maintenanceAuthor $Author: pgearon $
*
* @company <A href="mailto:info@PIsoftware.com">Plugged In Software</A>
*
* @copyright ©2001 <a href="http://www.pisoftware.com/">Plugged In
* Software Pty Ltd</a>
*
* @licence <a href="{@docRoot}/../../LICENCE">Mozilla Public License v1.1</a>
*/
public abstract class AbstractBlockFile implements BlockFile {
/** The endianness of this computer. */
public final static ByteOrder byteOrder;
/** The logger. */
private final static Logger logger = Logger.getLogger(AbstractBlockFile.class);
/** All the open files accessed as block files. */
private static Set<File> openFiles = new HashSet<File>();
/** Determine the byte order of this machine, and select an ordering to use. */
static {
String useByteOrderProp = System.getProperty("mulgara.xa.useByteOrder");
ByteOrder bo = ByteOrder.nativeOrder();
// Default.
if (useByteOrderProp != null) {
if (useByteOrderProp.equalsIgnoreCase("native")) {
bo = ByteOrder.nativeOrder();
} else if (useByteOrderProp.equalsIgnoreCase("big_endian")) {
bo = ByteOrder.BIG_ENDIAN;
} else if (useByteOrderProp.equalsIgnoreCase("little_endian")) {
bo = ByteOrder.LITTLE_ENDIAN;
} else {
logger.warn(
"Invalid value for property mulgara.xa.useByteOrder: " +
useByteOrderProp
);
}
}
byteOrder = bo;
}
/** The file of the BlockFile. */
protected File file;
/** A flag to indicate that the file is currently open. */
protected boolean isOpen = true;
/** The RandomAccessFile for this BlockFile. */
protected RandomAccessFile raf;
/** The FileChannel for this BlockFile. */
protected volatile FileChannel fc;
/** The size of a block in bytes. */
protected int blockSize;
/** The size of the file in blocks. */
protected long nrBlocks;
/**
* This constructor is only used by subclasses. Opens the file and will round it
* to an integral number of blocks if necessary.
*
* @param file The block file.
* @param blockSize The size of blocks in the block file.
* @throws IOException If an I/O error occurs.
*/
protected AbstractBlockFile(File file, int blockSize) throws IOException {
if (blockSize <= 0) {
throw new IllegalArgumentException("blockSize is zero or negative.");
}
this.file = file;
this.blockSize = blockSize;
synchronized (openFiles) {
if (openFiles.contains(file)) {
throw new IllegalArgumentException("File already open: " + file);
}
ensureOpen();
// Truncate the file to a multiple of blockSize if its size is not
// already a multiple of blockSize.
long fileSize = nrBlocks * blockSize;
if (fileSize < fc.size()) {
if (logger.isInfoEnabled()) {
logger.info(
"File size was not a multiple of blockSize: \"" + file + "\"."
);
}
MappingUtil.truncate(fc, fileSize);
}
openFiles.add(file);
}
}
/**
* Returns a BlockFile which represents an open file with the specified file
* name. If the file does not exist it will be created. This factory method
* allows the type of implementation to be selected.
*
* @param file the file to open.
* @param blockSize the size of a block in bytes.
* @param ioType The type of access to use on the file, memory mapped or with explicit IO.
* @return the open BlockFile.
* @throws IOException if an I/O error occurs.
*/
public static BlockFile openBlockFile(
File file, int blockSize, IOType ioType
) throws IOException {
String forceIOTypeProp = System.getProperty("mulgara.xa.forceIOType");
if (forceIOTypeProp != null) {
if (forceIOTypeProp.equalsIgnoreCase("mapped")) {
ioType = IOType.MAPPED;
} else if (forceIOTypeProp.equalsIgnoreCase("explicit")) {
ioType = IOType.EXPLICIT;
} else {
logger.warn(
"Invalid value for property mulgara.xa.forceIOType: " +
forceIOTypeProp
);
}
}
if (ioType == IOType.MAPPED) {
return new MappedBlockFile(file, blockSize);
} else if (ioType == IOType.EXPLICIT) {
return new IOBlockFile(file, blockSize);
} else {
throw new IllegalArgumentException("Invalid BlockFile ioType.");
}
}
/**
* Returns a BlockFile which represents an open file with the specified file
* name. If the file does not exist it will be created. This factory method
* allows the type of implementation to be selected.
*
* @param fileName the name of the file to open.
* @param blockSize the size of a block in bytes.
* @param ioType The type of access to use on the file, memory mapped or with explicit IO.
* @return the open BlockFile.
* @throws IOException if an I/O error occurs.
*/
public static BlockFile openBlockFile(
String fileName, int blockSize, IOType ioType
) throws IOException {
return openBlockFile(new File(fileName), blockSize, ioType);
}
/**
* Sets the length of the file in blocks. An implementation may defer
* changing the file size until a read or write operation is performed or the
* file is closed and may allow the actual disk file to be larger or smaller
* than this size while it is open but will set the file to this size when it
* is closed.
*
* @param nrBlocks the length of the file in blocks.
* @throws IOException if an I/O error occurs.
* @throws IllegalArgumentException if nrBlocks is invalid.
*/
public void setNrBlocks(long nrBlocks) throws IOException {
if (nrBlocks < 0) {
throw new IllegalArgumentException("nrBlocks is negative.");
}
this.nrBlocks = nrBlocks;
}
/**
* Gets the current length of the BlockFile in blocks. An implementation may
* allow the actual disk file to be larger or smaller than this size while it
* is open but will set the file to this size when it is closed.
*
* @return the current length of the BlockFile in blocks.
*/
public final long getNrBlocks() {
return nrBlocks;
}
/**
* Truncates the file to zero length.
*
* @throws IOException if an I/O error occurs.
*/
public void clear() throws IOException {
this.nrBlocks = 0;
try {
MappingUtil.truncate(fc, 0L);
fc.force(true);
} catch (ClosedChannelException ex) {
// The Channel may have been inadvertently closed by another thread
// being interrupted. Attempt to reopen the channel.
if (!ensureOpen()) {
throw ex;
}
}
}
/**
* Ensures that all data for this BlockFile is stored in persistent storage
* before returning.
*
* @throws IOException if an I/O error occurs.
*/
public void force() throws IOException {
try {
fc.force(true);
} catch (ClosedChannelException ex) {
// The Channel may have been inadvertently closed by another thread
// being interrupted. Attempt to reopen the channel.
if (!ensureOpen()) {
throw ex;
}
}
}
/**
* Normally used for copy-on-write, but not implemented for non-ManagedBlockFiles.
*
* @param block The Block to modify. Ignored.
*/
public void modifyBlock(Block block) {
throw new UnsupportedOperationException(
"modifyBlock() can only be applied to Blocks obtained from" +
" a ManagedBlockFile."
);
}
/**
* Normally used for freeing blocks that are no longer used, but not implemented for
* non-ManagedBlockFiles.
*
* @param blockId The block to free. Ignored.
*/
public void freeBlock(long blockId) {
throw new UnsupportedOperationException(
"Only Blocks obtained from a ManagedBlockFile can be freed."
);
}
/**
* Unmaps a file. Only applies to mapped files, as used by {@link MappedBlockFile}.
*/
public void unmap() {
// NO-OP
}
/**
* Closes the block file.
*
* @throws IOException if an I/O error occurs
*/
public synchronized void close() throws IOException {
if (!isOpen || (raf == null)) {
if (logger.isInfoEnabled()) {
logger.info("Attempt to close BlockFile that is already closed.");
}
return;
}
try {
unmap();
try {
MappingUtil.truncate(fc, nrBlocks * blockSize);
} catch (ClosedChannelException ex) {
// The Channel may have been inadvertently closed by another thread
// being interrupted. Attempt to reopen the channel.
if (!ensureOpen()) {
throw ex;
}
} catch (IOException ex) {
// The size should be corrected the next time the file is opened.
if (logger.isInfoEnabled()) {
logger.info(
"NOTE: Could not truncate file: \"" + file +
"\" to size: " + (nrBlocks * blockSize) +
" - deferring until next time the file is opened."
);
}
}
} finally {
try {
try {
if (fc != null && fc.isOpen()) fc.close();
} finally {
fc = null;
raf.close();
}
} finally {
raf = null;
synchronized (openFiles) {
openFiles.remove(file);
}
}
}
}
/**
* Close and delete the block file.
*
* @throws IOException if an I/O error occurs
*/
public synchronized void delete() throws IOException {
try {
close();
} finally {
if (file != null) {
int retries = 10;
while (!file.delete() && file.isFile() && retries-- > 0) {
// Causing any MappedByteBuffers to be unmapped may allow the
// file to be deleted. This may be needed for Windows.
MappingUtil.systemCleanup();
}
if (retries < 0) {
logger.warn("Failed to delete: " + file);
}
file = null;
}
}
}
/**
* Checks that a file is open. If it is not then attempts to open the file.
* Opening the file will also determine the size and number of blocks in the file.
*
* @return <code>true</code> if the file is open as a post-condition.i
* <code>false</code> if the file was not open and could not be opened.
* @throws IOException If there was an I/O error opening the file or getting its size.
*/
protected synchronized boolean ensureOpen() throws IOException {
// FIXME reenable aborting on interrupt when rollback is fixed.
// consume interrupt.
Thread.interrupted();
// if (Thread.interrupted()) throw new InterruptedIOException();
if (fc != null && fc.isOpen()) {
return true;
}
// Already open.
if (!isOpen) {
// File was closed deliberately or a previous reopen attempt failed.
return false;
}
// Make sure that the RandomAccessFile is closed.
isOpen = false;
if (raf != null) {
if (logger.isDebugEnabled()) {
logger.debug("Reopening: " + file);
}
try {
raf.close();
} finally {
raf = null;
}
}
// Try to open the file.
raf = new RandomAccessFile(file, "rw");
FileChannel newFC = raf.getChannel();
nrBlocks = newFC.size() / blockSize;
fc = newFC;
isOpen = true;
return true;
}
}