/*******************************************************************************
* Copyright 2011
* Ubiquitous Knowledge Processing (UKP) Lab
* Technische Universität Darmstadt
*
* 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 org.dkpro.lab.storage.filesystem;
import static org.dkpro.lab.engine.impl.ImportUtil.matchConstraints;
import static org.dkpro.lab.task.TaskContextMetadata.METADATA_KEY;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dkpro.lab.Util;
import org.dkpro.lab.engine.impl.ImportUtil;
import org.dkpro.lab.storage.StorageService;
import org.dkpro.lab.storage.StreamReader;
import org.dkpro.lab.storage.StreamWriter;
import org.dkpro.lab.storage.impl.PropertiesAdapter;
import org.dkpro.lab.task.Task;
import org.dkpro.lab.task.TaskContextMetadata;
import org.springframework.dao.DataAccessResourceFailureException;
/**
* Simple but effective file system-based storage service.
*/
public class FileSystemStorageService
implements StorageService
{
private final Log log = LogFactory.getLog(getClass());
private static final int MAX_RETRIES = 100;
private static final long SLEEP_TIME = 1000;
private File storageRoot;
public void setStorageRoot(File aStorageRoot)
{
storageRoot = aStorageRoot;
}
public File getStorageRoot()
{
return storageRoot;
}
@Override
public void delete(String aContextId)
{
try {
FileUtils.deleteDirectory(getContextFolder(aContextId, false));
}
catch (IOException e) {
throw new DataAccessResourceFailureException(e.getMessage(), e);
}
}
@Override
public void delete(String aContextId, String aKey)
{
try {
FileUtils.deleteDirectory(new File(getContextFolder(aContextId, false), aKey));
}
catch (IOException e) {
throw new DataAccessResourceFailureException(e.getMessage(), e);
}
}
@Override
public TaskContextMetadata getContext(String aContextId)
{
return retrieveBinary(aContextId, METADATA_KEY, new TaskContextMetadata());
}
@Override
public List<TaskContextMetadata> getContexts()
{
List<TaskContextMetadata> contexts = new ArrayList<TaskContextMetadata>();
for (File child : storageRoot.listFiles()) {
if (new File(child, METADATA_KEY).exists()) {
contexts.add(retrieveBinary(child.getName(), METADATA_KEY,
new TaskContextMetadata()));
}
}
Collections.sort(contexts, new Comparator<TaskContextMetadata>()
{
@Override
public int compare(TaskContextMetadata aO1, TaskContextMetadata aO2)
{
return Long.signum(aO2.getEnd() - aO1.getEnd());
}
});
return contexts;
}
@Override
public List<TaskContextMetadata> getContexts(String aTaskType, Map<String, String> aConstraints)
{
List<TaskContextMetadata> contexts = new ArrayList<TaskContextMetadata>();
nextContext: for (TaskContextMetadata e : getContexts()) {
// Ignore those that do not match the type
if (!aTaskType.equals(e.getType())) {
continue;
}
// Check the constraints if there are any
if (aConstraints.size() > 0) {
final Map<String, String> properties = retrieveBinary(e.getId(),
Task.DISCRIMINATORS_KEY, new PropertiesAdapter()).getMap();
if (!matchConstraints(properties, aConstraints, true)) {
continue nextContext;
}
}
contexts.add(e);
}
Collections.sort(contexts, new Comparator<TaskContextMetadata>()
{
@Override
public int compare(TaskContextMetadata aO1, TaskContextMetadata aO2)
{
return Long.signum(aO2.getEnd() - aO1.getEnd());
}
});
return contexts;
}
@Override
public TaskContextMetadata getLatestContext(String aTaskType, Map<String, String> aConstraints)
{
List<TaskContextMetadata> contexts = getContexts(aTaskType, aConstraints);
if (contexts.size() == 0) {
throw ImportUtil.createContextNotFoundException(aTaskType, aConstraints);
}
return contexts.get(0);
}
@Override
public boolean containsContext(String aContextId)
{
return getContextFolder(aContextId, false).isDirectory();
}
@Override
public boolean containsKey(String aContextId, String aKey)
{
return new File(getContextFolder(aContextId, false), aKey).exists();
}
@Override
public <T extends StreamReader> T retrieveBinary(String aContextId, String aKey, T aConsumer)
{
InputStream is = null;
int currentTry = 1;
IOException lastException = null;
while (currentTry <= MAX_RETRIES) {
try {
is = new FileInputStream(new File(getContextFolder(aContextId, true), aKey));
if (aKey.endsWith(".gz")) {
is = new GZIPInputStream(is);
}
aConsumer.read(is);
return aConsumer;
}
catch (IOException e) {
// https://code.google.com/p/dkpro-lab/issues/detail?id=64
// may be related to a concurrent access so try again after some time
lastException = e;
currentTry++;
log.debug(currentTry + ". try accessing " + aKey + " in context " + aContextId);
try {
Thread.sleep(SLEEP_TIME);
}
catch (InterruptedException e1) {
// we should probably abort the whole thing
currentTry = MAX_RETRIES;
}
}
catch (Throwable e) {
throw new DataAccessResourceFailureException("Unable to load [" + aKey
+ "] from context [" + aContextId + "]", e);
}
finally {
Util.close(is);
}
}
throw new DataAccessResourceFailureException("Unable to access [" + aKey + "] in context ["
+ aContextId + "]", lastException);
}
@Override
public void storeBinary(String aContextId, String aKey, StreamWriter aProducer)
{
File context = getContextFolder(aContextId, false);
File tmpFile = new File(context, aKey + ".tmp");
File finalFile = new File(context, aKey);
OutputStream os = null;
try {
tmpFile.getParentFile().mkdirs(); // Necessary if the key addresses a sub-directory
log.debug("Storing to: " + finalFile);
os = new FileOutputStream(tmpFile);
if (aKey.endsWith(".gz")) {
os = new GZIPOutputStream(os);
}
aProducer.write(os);
}
catch (Exception e) {
tmpFile.delete();
throw new DataAccessResourceFailureException(e.getMessage(), e);
}
finally {
Util.close(os);
}
// On some platforms, it is not possible to rename a file to another one which already
// exists. So try to delete the target file before renaming.
if (finalFile.exists()) {
boolean deleteSuccess = finalFile.delete();
if (!deleteSuccess) {
throw new DataAccessResourceFailureException("Unable to delete [" + finalFile
+ "] in order to replace it with an updated version.");
}
}
// Make sure the file is only visible under the final name after all data has been
// written into it.
boolean renameSuccess = tmpFile.renameTo(finalFile);
if (!renameSuccess) {
throw new DataAccessResourceFailureException("Unable to rename [" + tmpFile + "] to ["
+ finalFile + "]");
}
}
@Override
public void storeBinary(String aContextId, String aKey, final InputStream aStream)
{
try {
storeBinary(aContextId, aKey, new StreamWriter()
{
@Override
public void write(OutputStream aOs)
throws Exception
{
Util.shoveAndClose(aStream, aOs);
}
});
}
finally {
Util.close(aStream);
}
}
@Override
public File locateKey(String aContextId, String aKey)
{
return new File(getContextFolder(aContextId, false), aKey);
}
@Override
public File getStorageFolder(String aContextId, String aKey)
{
File folder = new File(getContextFolder(aContextId, false), aKey);
folder.mkdirs();
return folder;
}
public static boolean isStaticImport(URI uri)
{
if (LATEST_CONTEXT_SCHEME.equals(uri.getScheme())) {
return false;
}
else if (CONTEXT_ID_SCHEME.equals(uri.getScheme())) {
return false;
}
else {
return true;
}
}
@Override
public void copy(String aContextId, String aKey, StorageKey aResolvedKey, AccessMode aMode)
{
StorageKey key = aResolvedKey;
// If the resource is imported from another context and will be modified and it is a
// folder, we have to copy it to the current context now, since we cannot do a
// copy-on-write strategy as for streams.
if (isStorageFolder(key.contextId, key.key)
&& (aMode == AccessMode.READWRITE || aMode == AccessMode.ADD_ONLY)) {
try {
File source = new File(getContextFolder(key.contextId, false), key.key);
File target = new File(getContextFolder(aContextId, false), aKey);
if (Util.isSymlinkSupported() && aMode == AccessMode.ADD_ONLY) {
log.info("Write access to imported storage folder [" + aKey
+ "] was requested. Linking to current context");
Util.copy(source, target, true);
}
else {
log.info("Write access to imported storage folder [" + aKey
+ "] was requested. Copying to current context");
Util.copy(source, target, false);
}
}
catch (IOException e) {
throw new DataAccessResourceFailureException(e.getMessage(), e);
}
// Key should point to the local context now
key = new StorageKey(aContextId, aKey);
}
}
private File getContextFolder(String aContextId, boolean create)
{
File folder = new File(getStorageRoot(), aContextId);
if (create) {
folder.mkdirs();
}
return folder;
}
protected boolean isStorageFolder(String aContextId, String aKey)
{
return new File(getContextFolder(aContextId, false), aKey).isDirectory();
}
}