package com.laytonsmith.persistence;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.PureUtilities.DaemonManager;
import com.laytonsmith.annotations.datasource;
import com.laytonsmith.core.CHVersion;
import com.laytonsmith.persistence.io.ConnectionMixinFactory;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class provides a temporary data source that is in memory only, and is
* never written out to disk. All methods in this class are thread safe.
*/
@datasource("mem")
public final class MemoryDataSource extends AbstractDataSource {
private static final Map<String, Map<String, String>> databasePool = new TreeMap<String, Map<String, String>>();
/**
* Clears all data from all databases. Should be called when a natural reload type operation is called.
*/
public synchronized static void ClearDatabases(){
for(String s : databasePool.keySet()){
databasePool.get(s).clear();
}
databasePool.clear();
}
@Override
public void disconnect() {
ClearDatabases();
}
/**
* Retrieves the underlying database for a given database name.
* The actual database instance is returned, not a copy, and if
* the database doesn't exist, a new one is returned, therefore
* this will never return null.
* @param name
* @return
*/
public synchronized static Map<String, String> getDatabase(String name){
if(!databasePool.containsKey(name)){
databasePool.put(name, Collections.synchronizedMap(new TreeMap<String, String>()));
}
return databasePool.get(name);
}
private String dbName;
private List<Transaction> transactionList = new ArrayList<Transaction>();
private static enum Action {
CLEAR, SET
}
private static class Transaction {
public Action action;
public String key;
public String value;
}
private synchronized void addTransaction(Transaction transaction){
//If the transaction list contains this key already, we can clear it
//and add this one
Iterator<Transaction> it = transactionList.iterator();
while(it.hasNext()){
Transaction t = it.next();
if(t.key.equals(transaction.key)){
it.remove();
}
}
transactionList.add(transaction);
}
private synchronized void replayTransactions(){
for(Transaction t : transactionList){
try {
if(t.action == Action.CLEAR){
clearKey0(null, t.key.split("\\."));
} else if(t.action == Action.SET){
set0(null, t.key.split("\\."), t.value);
}
} catch (Exception ex) {
Logger.getLogger(MemoryDataSource.class.getName()).log(Level.SEVERE, null, ex);
}
}
transactionList.clear();
}
private MemoryDataSource(){
}
public MemoryDataSource(URI uri, ConnectionMixinFactory.ConnectionMixinOptions options) throws DataSourceException{
super(uri, options);
dbName = uri.getSchemeSpecificPart();
}
@Override
protected void startTransaction0(DaemonManager dm) {
//Don't need to actively do anything
}
@Override
protected synchronized void stopTransaction0(DaemonManager dm, boolean rollback) throws DataSourceException, IOException {
if(rollback){
transactionList.clear();
} else {
replayTransactions();
}
}
@Override
protected boolean set0(DaemonManager dm, String[] key, String value) throws ReadOnlyException, DataSourceException, IOException {
String fKey = StringUtils.Join(key, ".");
if(inTransaction()){
Transaction t = new Transaction();
t.action = Action.SET;
t.key = fKey;
t.value = value;
addTransaction(t);
} else {
getDatabase(dbName).put(fKey, value);
}
return true;
}
@Override
protected synchronized String get0(String[] key) throws DataSourceException {
String fKey = StringUtils.Join(key, ".");
if(inTransaction()){
for(Transaction t : transactionList){
if(t.action == Action.SET && t.key.equals(fKey)){
return t.value;
}
}
//Still not found, so it's an unaffected key.
//Simply return from the real database.
}
return getDatabase(dbName).get(fKey);
}
@Override
protected boolean hasKey0(String[] key) throws DataSourceException {
return get0(key) != null;
}
@Override
protected void clearKey0(DaemonManager dm, String[] key) throws ReadOnlyException, DataSourceException, IOException {
String fKey = StringUtils.Join(key, ".");
if(inTransaction()){
Transaction t = new Transaction();
t.action = Action.CLEAR;
t.key = fKey;
addTransaction(t);
} else {
getDatabase(dbName).remove(StringUtils.Join(key, "."));
}
}
@Override
public Set<String> stringKeySet(String[] keyBase) throws DataSourceException {
Set<String> keys = new TreeSet<String>();
for(String[] key : keySet(keyBase)){
keys.add(StringUtils.Join(key, "."));
}
return keys;
}
@Override
public synchronized Set<String[]> keySet(String[] keyBase) throws DataSourceException {
Set<String> set = new HashSet<String>();
for(String key : getDatabase(dbName).keySet()){
set.add(key);
}
//Now go through the transactions and add things that are set, and
//remove things that are cleared
if(inTransaction()){
for(Transaction t : transactionList){
if(t.action == Action.CLEAR){
set.remove(t.key);
} else if(t.action == Action.SET){
set.add(t.key);
}
}
}
Set<String[]> ret = new HashSet<String[]>();
String kb = StringUtils.Join(keyBase, ".");
for(String key : set){
if(key.startsWith(kb)){
ret.add(key.split("\\."));
}
}
return ret;
}
@Override
public void populate() throws DataSourceException {
//Ignored
}
@Override
public DataSourceModifier[] implicitModifiers() {
return new DataSourceModifier[]{};
}
@Override
public DataSourceModifier[] invalidModifiers() {
//No modifiers are appropriate on here
return DataSourceModifier.values();
}
@Override
public String docs() {
return "Temporary Memory {mem:databaseName} Creates a temporary database that exists in memory only. Since keys across"
+ " databases are always unique anyways, the name for databaseName is irrelevant, but is required, so"
+ " \"mem:default\" is a recommended configuration. There are no guarantees to how long the data will stay around (in either"
+ " how short of how long the data will be kept), except"
+ " that it is guaranteed that within an execution unit, that data will continue to exist. This causes it to"
+ " work much like import() and export(). Data stored this way is inaccessible to external processes, because it"
+ " exists only in the process's memory space.";
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
}