/*
* Copyright (c) 2017 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.magma.datasource.fs;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.Set;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import org.obiba.magma.Attribute;
import org.obiba.magma.MagmaEngine;
import org.obiba.magma.MagmaRuntimeException;
import org.obiba.magma.Value;
import org.obiba.magma.ValueTable;
import org.obiba.magma.ValueTableWriter;
import org.obiba.magma.datasource.crypt.DatasourceCipherFactory;
import org.obiba.magma.datasource.crypt.DatasourceEncryptionStrategy;
import org.obiba.magma.datasource.fs.input.CipherInputStreamWrapper;
import org.obiba.magma.datasource.fs.input.NullInputStreamWrapper;
import org.obiba.magma.datasource.fs.output.ChainedOutputStreamWrapper;
import org.obiba.magma.datasource.fs.output.CipherOutputStreamWrapper;
import org.obiba.magma.datasource.fs.output.DigestOutputStreamWrapper;
import org.obiba.magma.datasource.fs.output.NullOutputStreamWrapper;
import org.obiba.magma.support.AbstractDatasource;
import org.obiba.magma.type.BooleanType;
import org.obiba.magma.type.TextType;
import org.obiba.magma.xstream.MagmaXStreamExtension;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.thoughtworks.xstream.XStream;
import de.schlichtherle.io.ArchiveException;
import de.schlichtherle.io.ArchiveWarningException;
import de.schlichtherle.io.File;
import de.schlichtherle.io.FileInputStream;
import de.schlichtherle.io.FileOutputStream;
/**
* Implements a {@code Datasource} on top of an archive file in the local file system.
*/
@SuppressWarnings("OverlyCoupledClass")
public class FsDatasource extends AbstractDatasource {
/**
* Consistently use UTF-8 character set for reading and writing.
*/
private static final Charset CHARSET = Charset.availableCharsets().get("UTF-8");
private final File datasourceArchive;
@Nullable
private DatasourceEncryptionStrategy datasourceEncryptionStrategy;
private InputStreamWrapper inputStreamWrapper = new NullInputStreamWrapper();
private OutputStreamWrapper outputStreamWrapper = new NullOutputStreamWrapper();
private boolean instanceAttributesModified = false;
public FsDatasource(String name, java.io.File outputFile,
@Nullable DatasourceEncryptionStrategy datasourceEncryptionStrategy) {
this(name, outputFile);
this.datasourceEncryptionStrategy = datasourceEncryptionStrategy;
}
public FsDatasource(String name, java.io.File outputFile) {
super(name, "fs");
datasourceArchive = new File(outputFile);
}
public void setEncryptionStrategy(DatasourceEncryptionStrategy datasourceEncryptionStrategy) {
this.datasourceEncryptionStrategy = datasourceEncryptionStrategy;
}
@Override
protected void onInitialise() {
boolean newDatasource = true;
if(datasourceArchive.exists()) {
readAttributes();
newDatasource = false;
} else {
setAttributeValue("magma.datasource.fs.version", TextType.get().valueOf("1"));
setAttributeValue("magma.datasource.fs.encrypted",
hasEncryptionStrategy() ? BooleanType.get().trueValue() : BooleanType.get().falseValue());
}
// Setup cipher wrappers in the case where
initialiseEncrypted(newDatasource);
}
private void initialiseEncrypted(boolean newDatasource) {
if(isEncrypted() && hasEncryptionStrategy()) {
// Make sure our strategy is able to read an existing datasource.
if(datasourceEncryptionStrategy != null &&
(newDatasource || datasourceEncryptionStrategy.canDecryptExistingDatasource())) {
DatasourceCipherFactory cipherFactory = datasourceEncryptionStrategy.createDatasourceCipherFactory(this);
inputStreamWrapper = new CipherInputStreamWrapper(cipherFactory);
outputStreamWrapper = new ChainedOutputStreamWrapper(new CipherOutputStreamWrapper(cipherFactory),
new DigestOutputStreamWrapper());
} else {
throw new MagmaRuntimeException(
"Existing Datasource '" + getName() + "' cannot be decrypted using the specified encryption strategy.");
}
} else if(isEncrypted()) {
throw new MagmaRuntimeException(
"Datasource '" + getName() + "' is encrypted. An instance of DatasourceEncryptionStrategy must be provided.");
}
}
@Override
@NotNull
public ValueTableWriter createWriter(@NotNull String name, @NotNull String entityType) {
FsValueTable valueTable = null;
if(hasValueTable(name)) {
valueTable = (FsValueTable) getValueTable(name);
} else {
addValueTable(valueTable = new FsValueTable(this, name, entityType));
}
return new FsValueTableWriter(valueTable, getXStreamInstance());
}
@Override
public void onDispose() {
try {
if(instanceAttributesModified) {
writeAttributes();
}
} finally {
try {
File.umount(datasourceArchive);
} catch(ArchiveWarningException e) {
// ArchiveWarningException are non-fatal. We choose to ignore them.
} catch(ArchiveException e) {
throw new MagmaRuntimeException(e);
}
}
}
@Override
public void setAttributeValue(String name, Value value) {
getInstanceAttributes().put(name, Attribute.Builder.newAttribute(name).withValue(value).build());
instanceAttributesModified = true;
}
protected boolean hasEncryptionStrategy() {
return datasourceEncryptionStrategy != null;
}
protected boolean isEncrypted() {
if(hasAttribute("magma.datasource.fs.encrypted")) {
Value value = getAttributeValue("magma.datasource.fs.encrypted");
//noinspection ConstantConditions
return !value.isNull() && (Boolean) value.getValue();
}
return false;
}
@SuppressWarnings("unchecked")
protected void readAttributes() {
try(Reader reader = new InputStreamReader(new FileInputStream(new File(datasourceArchive, "metadata.xml")),
CHARSET)) {
Iterable<Attribute> attributes = (Iterable<Attribute>) getXStreamInstance().fromXML(reader);
for(Attribute a : attributes) {
getInstanceAttributes().put(a.getName(), a);
}
} catch(IOException e) {
throw new MagmaRuntimeException(e);
}
}
protected void writeAttributes() {
try(Writer writer = new OutputStreamWriter(new FileOutputStream(new File(datasourceArchive, "metadata.xml")),
CHARSET)) {
getXStreamInstance().toXML(new LinkedList<>(getInstanceAttributes().values()), writer);
instanceAttributesModified = false;
} catch(IOException e) {
throw new MagmaRuntimeException(e);
}
}
@Override
protected Set<String> getValueTableNames() {
if(datasourceArchive.exists()) {
java.io.File[] files = datasourceArchive.listFiles(new FileFilter() {
@Override
public boolean accept(java.io.File pathname) {
return pathname.isDirectory();
}
});
Set<String> tableNames = Sets.newHashSet();
for(java.io.File f : files) {
tableNames.add(f.getName());
}
return tableNames;
}
return ImmutableSet.of();
}
@Override
protected ValueTable initialiseValueTable(String tableName) {
return new FsValueTable(this, tableName);
}
File getEntry(String name) {
return new File(datasourceArchive, name);
}
XStream getXStreamInstance() {
// TODO: Use the FsDatasource version to obtain the proper XStream instance
return MagmaEngine.get().getExtension(MagmaXStreamExtension.class).getXStreamFactory().createXStream();
}
@Nullable
<T> T readEntry(File entry, InputCallback<T> callback) {
if(entry.exists()) {
try(Reader reader = createReader(entry)) {
return callback.readEntry(reader);
} catch(IOException e) {
throw new MagmaRuntimeException(e);
}
}
return null;
}
<T> T writeEntry(File file, OutputCallback<T> callback) {
try(Writer writer = createWriter(file)) {
return callback.writeEntry(writer);
} catch(IOException e) {
throw new MagmaRuntimeException(e);
}
}
Reader createReader(File entry) {
try {
return new InputStreamReader(inputStreamWrapper.wrap(new FileInputStream(entry), entry), CHARSET);
} catch(FileNotFoundException e) {
throw new MagmaRuntimeException(e);
}
}
Writer createWriter(File entry) {
try {
return new OutputStreamWriter(outputStreamWrapper.wrap(new FileOutputStream(entry), entry), CHARSET);
} catch(FileNotFoundException e) {
throw new MagmaRuntimeException(e);
}
}
interface InputCallback<T> {
T readEntry(Reader reader) throws IOException;
}
interface OutputCallback<T> {
T writeEntry(Writer writer) throws IOException;
}
}