package com.laytonsmith.persistence.io;
import com.laytonsmith.PureUtilities.Common.FileUtil;
import com.laytonsmith.PureUtilities.DaemonManager;
import com.laytonsmith.PureUtilities.ZipReader;
import com.laytonsmith.persistence.ReadOnlyException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.log4j.lf5.util.StreamUtils;
/**
*
*/
public class ReadWriteFileConnection implements ConnectionMixin {
//Do not change the name of this. It is read reflectively during testing
protected final File file;
/**
* The encoding that was determined to be the encoding for this file, if
* set, or UTF-8 by default, if the file doesn't exist.
*/
protected String encoding = "UTF-8";
protected final ZipReader reader;
protected final String blankDataModel;
protected final ExecutorService service;
/**
* The executor service allows for reads and writes to be synchronized.
* Writes needn't be synchronous, just merely synchronized with reads and
* other writes. Reads of course need to be synchronous, at least as far as
* the thread that runs the getData function is concerned, but we still need
* to actually run the task on the executor service thread, so it will be
* synced with the writes. All file based persistence systems should use
* this executor to do the reads and writes.
*/
public ReadWriteFileConnection(URI uri, File workingDirectory, String blankDataModel) throws IOException {
{
//This bit is a little tricky. Since this is a file path, not a URL, we can't use most of the parts
//of the URI class. We need to get the scheme specific part directly, and parse it ourselves. Given
//"sqlite://../path/to/db.db" getSchemeSpecificPart() will return "//../path/to/db.db" so we need to
//check to see if it starts with "//" (either 2 or 3 slashes are acceptable) and remove those manually.
//then the rest of the path is the actual file path. If it is absolute, the File constructor will handle
//that for us.
String path = uri.getSchemeSpecificPart();
if (!path.startsWith("//")) {
throw new IOException("Could not read the URI: " + uri.toString() + ". Did you forget the \"//\"?");
}
path = path.substring(2);
File temp = new File(path);
if (temp.isAbsolute()) {
file = temp;
} else {
file = new File(workingDirectory, path);
}
}
if (file.exists()) {
encoding = FileUtil.getFileCharset(file);
}
reader = new ZipReader(file);
if (!reader.isZipped()) {
if (reader.getTopLevelFile().getParentFile() != null) {
reader.getTopLevelFile().getParentFile().mkdirs();
}
}
if (!reader.exists()) {
reader.getTopLevelFile().createNewFile();
}
this.blankDataModel = blankDataModel;
this.service = new ThreadPoolExecutor(1, 1,
60L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "ReadWriteFileConnection-" + file);
t.setPriority(Thread.MIN_PRIORITY);
t.setDaemon(true);
return t;
}
});
}
@Override
@SuppressWarnings("ThrowableResultIgnored")
public String getData() throws IOException {
if (reader.isZipped()) {
//We have an entirely different method here: it is assumed that
//a zip file is an archive; that is, there will be no write operations.
//Making this assumption, it is then OK to simply read from it
//without worrying about corruption from a write operation.
return reader.getFileContents();
}
final Future<byte[]> future;
synchronized (service) {
future = service.submit(new Callable<byte[]>() {
@Override
public byte[] call() throws Exception {
return StreamUtils.getBytes(FileUtil.readAsStream(file));
}
});
}
try {
return new String(future.get(), encoding);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
} catch (ExecutionException ex) {
if (ex.getCause() instanceof IOException) {
throw (IOException) ex.getCause();
} else {
throw new RuntimeException(ex.getCause());
}
}
}
@Override
public void writeData(final DaemonManager dm, final String data) throws ReadOnlyException, IOException, UnsupportedOperationException {
if (reader.isZipped()) {
throw new ReadOnlyException("Cannot write to a zipped file.");
}
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
if (!file.exists()) {
throw new FileNotFoundException(file.getAbsolutePath() + " does not exist!");
}
synchronized (service) {
dm.activateThread(null);
service.submit(new Runnable() {
@Override
public void run() {
try {
FileUtil.write(data, file);
} catch (IOException ex) {
Logger.getLogger(ReadWriteFileConnection.class.getName()).log(Level.SEVERE, null, ex);
}
dm.deactivateThread(null);
}
});
}
}
@Override
public String getPath() throws UnsupportedOperationException, IOException {
return file.getCanonicalPath();
}
}