/*-
*******************************************************************************
* Copyright (c) 2011, 2014 Diamond Light Source Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthew Gerring - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.dawnsci.hdf.object;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.dawnsci.hdf5.HDF5FileFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import hdf.object.FileFormat;
/**
* This class should be used to access HDF5 files from Java as long as the limitations like those
* in HDF 2.7 are in place. This class gives a facade which is designed to ensure no more that
* one file handle to a hdf5 file is active at one time - however multiple threads may access
* the file. It is better than alternative ways of doing this as the level of synchronization is
* lower. Use HierarchicalDataFactory as much as possible to avoid thread problems with HDF5.
*
*
* <usage><code>
* HierarchicalDataFile file = null;
* try {
* file = HierarchicalDataFactory.getXXX();
*
* ... use file
*
* } finally {
* file.close();
* }
* </code></usage>
*
* @author Matthew Gerring
* @deprecated Please use ILazyWriteableDataset to write and ILoaderService to read.
*/
@Deprecated
public class HierarchicalDataFactory {
private static final Logger logger = LoggerFactory.getLogger(HierarchicalDataFactory.class);
/**
* Canonicalise path so that we can use it as a standard key
* @param absolutePath
* @return
* @throws IOException
*/
public static String canonicalisePath(String absolutePath) throws IOException {
try {
return new File(absolutePath).getCanonicalPath();
} catch (IOException e) {
logger.error("Could not get canonical path: {}", absolutePath);
throw e;
}
}
/**
* Call this method to get a reference to a HierarchicalDataFile
* opened for reading use.
*
* @param absolutePath
* @return
* @throws Exception
*/
public static IHierarchicalDataFile getReader(final String absolutePath) throws Exception {
return HierarchicalDataFactory.getReader(absolutePath, false);
}
/**
* Get the reader, optionally waiting if a low level API call is blocking reading the file.
* @param path absolute path
* @param waitForLowLevel
* @return
* @throws Exception
*/
public static IHierarchicalDataFile getReader(final String path, boolean waitForLowLevel) throws Exception {
String absolutePath = canonicalisePath(path);
// FIXME temporary fix to stop overlapping high and low level uses of HDF5 library
HDF5FileFactory.releaseFile(absolutePath, true);
if (lowLevelLocks.containsKey(absolutePath)) {
if (!waitForLowLevel) {
throw new Exception("The low level API is currently reading from "+absolutePath);
} else {
final ReentrantLock lock = lowLevelLocks.get(absolutePath);
if (lock!=null) {
try {
lock.lock(); // waits
return HierarchicalDataFile.open(absolutePath, FileFormat.READ);
} finally {
lock.unlock();
lowLevelLocks.remove(absolutePath);
}
}
}
}
return HierarchicalDataFile.open(absolutePath, FileFormat.READ);
}
/**
* Expert use only. This will close all writers and reader references for a give
* file path. Use with Caution because other references to the file may exist,
* which will go dead.
*/
public static void closeReaders(final String path) throws Exception {
String absolutePath = canonicalisePath(path);
HierarchicalDataFile.closeReaders(absolutePath);
}
/**
* Call this method to get a reference to a HierarchicalDataFile
* opened for writing use.
*
* @param absolutePath
* @return
* @throws Exception
*/
public static IHierarchicalDataFile getWriter(final String absolutePath) throws Exception {
return getWriter(absolutePath, false);
}
/**
* Call this method to get a reference to a HierarchicalDataFile
* opened for writing use.
*
* @param path absolute path
* @param waitForAvailability if false and in use, exception thrown, if true will wait for lock in high level API. If a lock of the low level API is active, throws exception regardless.
* @return
* @throws Exception
*/
public static IHierarchicalDataFile getWriter(final String path, boolean waitForAvailability) throws Exception {
String absolutePath = canonicalisePath(path);
if (!(new File(absolutePath)).exists()) {
create(absolutePath);
}
if (lowLevelLocks.containsKey(absolutePath)) throw new Exception("The low level API is currently reading from "+absolutePath);
return HierarchicalDataFile.open(absolutePath, FileFormat.WRITE, waitForAvailability);
}
/**
* Call this method to get a reference to a *new* HierarchicalDataFile.
*
* Do
*
* @param path
* @return
* @throws Exception
*/
public static void create(final String path) throws Exception {
String absolutePath = canonicalisePath(path);
IHierarchicalDataFile file=null;
try { // Try finally not really necessary but sets good example.
file = HierarchicalDataFile.open(absolutePath, FileFormat.CREATE);
} finally {
if (file!=null) file.close();
}
}
public static boolean isHDF5(final String path) {
String absolutePath;
try {
absolutePath = canonicalisePath(path);
} catch (IOException e) {
return false;
}
if (HierarchicalDataFile.isWriting(absolutePath)) return true;
if (HierarchicalDataFile.isReading(absolutePath)) return true;
// We guess based on extension
final String lowPath = absolutePath.toLowerCase();
for (String ext : EXT) {
if (lowPath.endsWith(ext)) return true;
}
return false;
}
private final static List<String> EXT;
static {
EXT = new ArrayList<String>(7);
EXT.add(".h5");
EXT.add(".nxs");
EXT.add(".hd5");
EXT.add(".hdf5");
EXT.add(".hdf");
EXT.add(".nexus");
}
/**
* These locks can be used for requesting a lock for accessing a file path.
*/
private static Map<String, ReentrantLock> lowLevelLocks = new Hashtable<String, ReentrantLock>();
private static ReentrantLock accessLock = new ReentrantLock();
/**
* Ask to acquire a lock on a given file path. This call will block until
* other loaders have finished accessing the file.
*
* This lock effectively forces the code asking for the lock into a single threaded mode.
* Only one thread may hold the lock at a time. This is useful when using the low level
* API which will not like multiple threads holding the same file handle and will very
* likely crash if this happens.
*
* If the high level API holds a lock (its concept of locking is different and not single threaded) then
* we will attempt to close this lock before getting a lock on the low level API.
*
* Expert use only. acquireLowLevelReadingAccess and releaseLowLevelReadingAccess
* must be used in a try{} finally{} block.
*
* @param path absolute path
*/
public static void acquireLowLevelReadingAccess(final String path) throws Exception {
String absolutePath = canonicalisePath(path);
accessLock.lock();
ReentrantLock l;
try {
// If the high level has the lock, we attempt to close it
// which hopefully should avoid a crash.
if (HierarchicalDataFile.isReading(absolutePath)) {
closeReaders(absolutePath);
}
l = lowLevelLocks.get(absolutePath);
// Important to remove our low level lock at this point or deadlocks occur.
if (HierarchicalDataFile.isReading(absolutePath)) {
if (l!=null) try {
l.unlock();
} catch (Exception ignored) {
}
lowLevelLocks.remove(absolutePath);
throw new Exception("The file path "+absolutePath+" is already being used by the high level API!");
}
logger.trace(String.format("Get lock for %s (thd %x)", absolutePath, Thread.currentThread().getId()));
if (l == null) {
l = new ReentrantLock();
logger.trace(" Lock created for {}", absolutePath);
lowLevelLocks.put(absolutePath, l);
} else {
logger.trace(String.format(" Lock exists for %s (%b)", absolutePath, l.isLocked()));
}
} finally {
accessLock.unlock();
}
if (l.tryLock()) {
logger.trace(String.format(" Lock free for %s (or held by current thd %x)", absolutePath, Thread.currentThread().getId()));
} else {
logger.trace(" Wait for held lock for {}", absolutePath);
l.lock();
logger.trace(String.format(" Hold lock for %s (thd %x)", absolutePath, Thread.currentThread().getId()));
}
}
/**
* Ask to release a lock on a given file path. If this thread already
* holds the lock and has a count greater than 1 the thread will keep the lock.
*
* Expert use only. acquireLowLevelReadingAccess and releaseLowLevelReadingAccess
* must be used in a try{} finally{} block.
*
* @param path absolute path
*/
public static void releaseLowLevelReadingAccess(final String path) {
String absolutePath;
try {
absolutePath = canonicalisePath(path);
} catch (IOException e) {
logger.error("Ignoring exception whilst releasing read lock", e);
return;
}
accessLock.lock();
try {
ReentrantLock l = lowLevelLocks.get(absolutePath);
if (l != null) {
if (l.isHeldByCurrentThread()) {
l.unlock();
logger.trace(String.format("Release lock for %s (thd %x, %d)", absolutePath, Thread.currentThread().getId(), l.getHoldCount()));
} else {
logger.trace("Somehow the lock for {} was released (thd {})!", absolutePath, Thread.currentThread().getId());
}
if (!l.hasQueuedThreads()) {
lowLevelLocks.remove(absolutePath);
}
l = null;
}
} finally {
accessLock.unlock();
}
}
}