/*****************************************************************************
* Copyright (c) 2006-2013, Cloudsmith Inc.
* The code, documentation and other materials contained herein have been
* licensed under the Eclipse Public License - v 1.0 by the copyright holder
* listed above, as the Initial Contributor under such license. The text of
* such license is available at www.eclipse.org.
*****************************************************************************/
package org.eclipse.buckminster.core.metadata;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.eclipse.buckminster.core.CorePlugin;
import org.eclipse.buckminster.core.Messages;
import org.eclipse.buckminster.core.helpers.FileUtils;
import org.eclipse.buckminster.core.metadata.model.ElementNotFoundException;
import org.eclipse.buckminster.core.parser.IParser;
import org.eclipse.buckminster.runtime.BuckminsterException;
import org.eclipse.buckminster.runtime.IOUtils;
import org.eclipse.buckminster.runtime.Trivial;
import org.eclipse.buckminster.sax.UUIDKeyed;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.osgi.util.NLS;
/**
* @author Thomas Hallgren
*/
public class FileStorage<T extends UUIDKeyed> extends AbstractSaxableStorage<T> {
public static class Lock {
public static Lock lock(File file, boolean exclusive) throws CoreException {
return new Lock(file, exclusive);
}
private final RandomAccessFile lockFile;
private FileLock lock;
private Lock(File file, boolean exclusive) throws CoreException {
RandomAccessFile raFile = null;
try {
raFile = new RandomAccessFile(file, "rws"); //$NON-NLS-1$
lock = raFile.getChannel().lock(0, Long.MAX_VALUE, !exclusive);
lockFile = raFile;
} catch (IOException e) {
IOUtils.close(raFile);
throw BuckminsterException.wrap(e);
}
}
public FileChannel getLockChannel() {
return lock.channel();
}
public RandomAccessFile getLockFile() {
return lockFile;
}
public void migrateToExclusive() throws CoreException {
if (lock.isShared()) {
FileLock shared = lock;
try {
FileChannel channel = shared.channel();
shared.release();
lock = channel.lock(0, Long.MAX_VALUE, true);
} catch (IOException e) {
throw BuckminsterException.wrap(e);
}
}
}
public void release() {
try {
lock.release();
} catch (IOException e) {
}
IOUtils.close(lockFile);
}
}
private final HashMap<UUID, TimestampedKey> timestamps = new HashMap<UUID, TimestampedKey>();
private static final String SEQUENCE_FILE = ".sqfile"; //$NON-NLS-1$
private final File folder;
private final File sqFile;
private final HashMap<UUID, T> parsed = new HashMap<UUID, T>();
private transient T[] allElements;
private final IParser<T> parser;
private long cacheTime;
private long lastChecked;
private boolean sequenceChanged;
public FileStorage(File folder, IParser<T> parser, Class<T> clazz, int sequenceNumber) throws CoreException {
super(clazz);
this.folder = folder;
this.sqFile = new File(folder, SEQUENCE_FILE);
this.parser = parser;
FileUtils.createDirectory(folder, null);
Lock lock = Lock.lock(sqFile, true);
try {
ByteBuffer bf = ByteBuffer.allocateDirect(4);
bf.order(ByteOrder.LITTLE_ENDIAN);
FileChannel fc = lock.getLockChannel();
int foundSequenceNumber = fc.read(bf) == 4 ? bf.getInt(0) : -1;
sequenceChanged = (foundSequenceNumber >= 0 && foundSequenceNumber != sequenceNumber);
if (foundSequenceNumber < 0 || sequenceChanged) {
// Use exclusive lock and write the new sequence number
//
bf.clear();
bf.putInt(sequenceNumber);
bf.flip();
fc.position(0);
fc.write(bf);
}
File[] files = folder.listFiles();
int idx = files.length;
while (--idx >= 0) {
File file = files[idx];
String name = file.getName();
if (name.charAt(0) == '.')
continue;
UUID id = UUID.fromString(name);
timestamps.put(id, new TimestampedKey(id, file.lastModified()));
}
} catch (IOException e) {
throw BuckminsterException.wrap(e);
} finally {
lock.release();
}
cacheTime = sqFile.lastModified();
lastChecked = System.currentTimeMillis();
}
@Override
public synchronized void clear() {
try {
Lock lock = Lock.lock(sqFile, true);
try {
for (File file : folder.listFiles())
if (!file.equals(sqFile))
file.delete();
} finally {
lock.release();
}
} catch (CoreException e) {
CorePlugin.getLogger().error(e, e.toString());
}
parsed.clear();
timestamps.clear();
}
@Override
public synchronized boolean contains(T element) throws CoreException {
checkCache();
return timestamps.containsKey(element.getId());
}
@Override
public synchronized long getCreationTime(UUID elementId) throws ElementNotFoundException {
checkCache();
TimestampedKey tsKey = timestamps.get(elementId);
if (tsKey == null)
throw new ElementNotFoundException(this, elementId);
return tsKey.getCreationTime();
}
@Override
public synchronized T getElement(UUID elementId) throws CoreException {
checkCache();
if (!timestamps.containsKey(elementId))
throw new ElementNotFoundException(this, elementId);
T element = parsed.get(elementId);
if (element != null)
return element;
Lock lock = Lock.lock(sqFile, false);
InputStream input = null;
try {
File elementFile = getElementFile(elementId);
input = new FileInputStream(elementFile);
element = parser.parse(elementFile.toString(), input);
element.setId(elementId);
parsed.put(elementId, element);
return element;
} catch (FileNotFoundException e) {
throw new ElementNotFoundException(this, elementId);
} finally {
IOUtils.close(input);
lock.release();
}
}
@Override
public synchronized T[] getElements() throws CoreException {
checkCache();
if (allElements == null) {
Set<UUID> keys = timestamps.keySet();
Set<UUID> badKeys = null;
int idx = keys.size();
T[] elems = createArray(idx);
for (UUID key : keys) {
try {
--idx;
elems[idx] = getElement(key);
} catch (CoreException e) {
CorePlugin.getLogger().warning(BuckminsterException.unwind(e), NLS.bind(Messages.Unable_to_read_0, getElementClass().getName()));
if (badKeys == null)
badKeys = new HashSet<UUID>();
badKeys.add(key);
}
}
if (badKeys != null) {
idx = elems.length;
int goodIdx = idx - badKeys.size();
T[] goodElems = createArray(goodIdx);
while (--idx >= 0) {
T elem = elems[idx];
if (elem != null)
goodElems[--goodIdx] = elem;
}
elems = goodElems;
for (UUID badKey : badKeys)
timestamps.remove(badKey);
}
allElements = elems;
}
return allElements;
}
@Override
public synchronized UUID[] getKeys() {
checkCache();
Set<UUID> keys = timestamps.keySet();
return keys.toArray(new UUID[keys.size()]);
}
@Override
public String getName() {
return folder.getName();
}
@Override
public synchronized List<UUID> getReferencingKeys(UUID foreignKey, String keyName) throws CoreException {
List<UUID> result = null;
Method getter = getGetter(keyName);
try {
for (UUID elementId : timestamps.keySet()) {
T element = getElement(elementId);
UUID fkey = (UUID) getter.invoke(element, Trivial.EMPTY_OBJECT_ARRAY);
if (fkey != null && fkey.equals(foreignKey)) {
if (result == null)
result = new ArrayList<UUID>();
result.add(elementId);
}
}
if (result == null)
result = Collections.emptyList();
return result;
} catch (Exception e) {
throw BuckminsterException.wrap(e);
}
}
@Override
public synchronized TimestampedKey[] getTimestampedKeys() {
checkCache();
Collection<TimestampedKey> values = timestamps.values();
return values.toArray(new TimestampedKey[values.size()]);
}
@Override
public synchronized void putElement(T element) throws CoreException {
UUID id = element.getId();
long timestamp;
if (!timestamps.containsKey(id)) {
parsed.put(id, element);
persistImage(id, element.getImage());
timestamp = System.currentTimeMillis();
} else {
timestamp = System.currentTimeMillis();
getElementFile(id).setLastModified(timestamp);
}
timestamps.put(id, new TimestampedKey(id, timestamp));
}
@Override
public synchronized void putElement(UUID id, T element) throws CoreException {
UUID realId = element.getId();
putElement(element);
if (id.equals(realId))
return;
// A discreprancy has occured between elements. Likely due to
// different XML versions.
//
CorePlugin.getLogger().debug("Element id discrepancy in storage %s, expected %s, was %s", getName(), realId, id); //$NON-NLS-1$
if (timestamps.containsKey(id))
return;
parsed.put(id, element);
persistImage(id, element.getImage());
timestamps.put(id, new TimestampedKey(id, System.currentTimeMillis()));
}
@Override
public synchronized void removeElement(UUID elementId) throws CoreException {
parsed.remove(elementId);
persistImage(elementId, null);
}
@Override
public boolean sequenceChanged() {
return sequenceChanged;
}
private void checkCache() {
// The lastModified() call is an IO call. We don't want that to happen
// too frequently so we use the lastChecked to limit it to no more
// then
// once a second.
//
long now = System.currentTimeMillis();
if (now - lastChecked < 1000)
return;
if (cacheTime >= sqFile.lastModified()) {
lastChecked = now;
return;
}
try {
Lock lock = Lock.lock(sqFile, false);
try {
timestamps.clear();
parsed.clear();
allElements = null;
File[] files = folder.listFiles();
int idx = files.length;
while (--idx >= 0) {
File file = files[idx];
String name = file.getName();
if (name.charAt(0) == '.')
continue;
UUID id = UUID.fromString(name);
timestamps.put(id, new TimestampedKey(id, file.lastModified()));
}
cacheTime = sqFile.lastModified();
lastChecked = System.currentTimeMillis();
} finally {
lock.release();
}
} catch (CoreException e) {
CorePlugin.getLogger().error(e, e.getMessage());
}
}
private File getElementFile(UUID elementId) {
return new File(folder, elementId.toString());
}
private void persistImage(UUID elementId, byte[] image) throws CoreException {
Lock lock = Lock.lock(sqFile, true);
try {
allElements = null;
File elementFile = getElementFile(elementId);
if (image == null) {
if (!elementFile.delete() && elementFile.exists())
throw new FileUtils.DeleteException(elementFile);
timestamps.remove(elementId);
} else {
OutputStream output = null;
try {
output = new FileOutputStream(elementFile);
output.write(image);
} catch (IOException e) {
elementFile.delete();
throw BuckminsterException.wrap(e);
} finally {
IOUtils.close(output);
}
}
cacheTime = System.currentTimeMillis();
lastChecked = cacheTime;
sqFile.setLastModified(cacheTime);
} finally {
lock.release();
}
}
}