package com.laytonsmith.core.functions;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.annotations.api;
import com.laytonsmith.annotations.core;
import com.laytonsmith.annotations.noboilerplate;
import com.laytonsmith.annotations.seealso;
import com.laytonsmith.core.CHLog;
import com.laytonsmith.core.CHVersion;
import com.laytonsmith.core.LogLevel;
import com.laytonsmith.core.constructs.CArray;
import com.laytonsmith.core.constructs.CBoolean;
import com.laytonsmith.core.constructs.CNull;
import com.laytonsmith.core.constructs.CString;
import com.laytonsmith.core.constructs.CVoid;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.environments.Environment;
import com.laytonsmith.core.environments.GlobalEnv;
import com.laytonsmith.core.exceptions.CRE.CREFormatException;
import com.laytonsmith.core.exceptions.CRE.CREIOException;
import com.laytonsmith.core.exceptions.CRE.CREInsufficientArgumentsException;
import com.laytonsmith.core.exceptions.CRE.CREThrowable;
import com.laytonsmith.core.exceptions.CancelCommandException;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.exceptions.MarshalException;
import com.laytonsmith.persistence.DataSourceException;
import com.laytonsmith.persistence.PersistenceNetwork;
import com.laytonsmith.persistence.ReadOnlyException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
*/
@core
public class Persistence {
public static String docs() {
return "Allows scripts to store data from execution to execution. See the guide on [[CommandHelper/Persistence|persistence]] for more information."
+ " In all the functions, you may send multiple arguments for the key, which will automatically"
+ " be concatenated with a period (the namespace separator). No magic happens here, you can"
+ " put periods yourself, or combine manually namespaced values or automatically namespaced values"
+ " with no side effects. All the functions in the Persistence API are threadsafe (though not necessarily"
+ " process safe).";
}
@api(environments={GlobalEnv.class})
@noboilerplate
@seealso({get_value.class, clear_value.class, com.laytonsmith.tools.docgen.templates.PersistenceNetwork.class})
public static class store_value extends AbstractFunction {
@Override
public String getName() {
return "store_value";
}
@Override
public Integer[] numArgs() {
return new Integer[]{Integer.MAX_VALUE};
}
@Override
public String docs() {
return "void {[namespace, ...,] key, value} Allows you to store a value, which can then be retrieved later. key must be a string containing"
+ " only letters, numbers, underscores. Periods may also be used, but they form a namespace, and have special meaning."
+ " (See get_values())";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREFormatException.class, CREIOException.class, CREFormatException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public CHVersion since() {
return CHVersion.V3_0_2;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException {
String key = GetNamespace(args, args.length - 1, getName(), t);
String value = null;
try {
value = Construct.json_encode(args[args.length - 1], t);
} catch (MarshalException e) {
throw new CREFormatException(e.getMessage(), t);
}
char pc = '.';
for (int i = 0; i < key.length(); i++) {
Character c = key.charAt(i);
if (i != 0) {
pc = key.charAt(i - 1);
}
if ((i == 0 || i == key.length() - 1 || pc == '.') && c == '.') {
throw new CREFormatException("Periods may only be used as seperators between namespaces.", t);
}
if (c != '_' && c != '.' && !Character.isLetterOrDigit(c)) {
throw new CREFormatException("Param 1 in store_value must only contain letters, digits, underscores, or dots, (which denote namespaces).", t);
}
}
CHLog.GetLogger().Log(CHLog.Tags.PERSISTENCE, LogLevel.DEBUG, "Storing: " + key + " -> " + value, t);
try {
env.getEnv(GlobalEnv.class).GetPersistenceNetwork().set(env.getEnv(GlobalEnv.class).GetDaemonManager(), ("storage." + key).split("\\."), value);
} catch(IllegalArgumentException e){
throw new CREFormatException(e.getMessage(), t);
} catch (Exception ex) {
throw new CREIOException(ex.getMessage(), t, ex);
}
return CVoid.VOID;
}
@Override
public Boolean runAsync() {
//Because we do IO
return true;
}
@Override
public LogLevel profileAt() {
return LogLevel.DEBUG;
}
}
@api(environments={GlobalEnv.class})
@noboilerplate
@seealso({store_value.class, get_values.class, has_value.class, com.laytonsmith.tools.docgen.templates.PersistenceNetwork.class})
public static class get_value extends AbstractFunction {
@Override
public String getName() {
return "get_value";
}
@Override
public Integer[] numArgs() {
return new Integer[]{Integer.MAX_VALUE};
}
@Override
public String docs() {
return "Mixed {[namespace, ...,] key} Returns a stored value stored with store_value. If the key doesn't exist in storage, null"
+ " is returned. On a more detailed note: If the value stored in the persistence database is not actually a construct,"
+ " then null is also returned.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class, CREFormatException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public CHVersion since() {
return CHVersion.V3_0_2;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException {
Object o;
String namespace = GetNamespace(args, null, getName(), t);
CHLog.GetLogger().Log(CHLog.Tags.PERSISTENCE, LogLevel.DEBUG, "Getting value: " + namespace, t);
try {
Object obj;
try {
obj = env.getEnv(GlobalEnv.class).GetPersistenceNetwork().get(("storage." + namespace).split("\\."));
} catch (DataSourceException ex) {
throw new CREIOException(ex.getMessage(), t, ex);
} catch(IllegalArgumentException e){
throw new CREFormatException(e.getMessage(), t, e);
}
if (obj == null) {
return CNull.NULL;
}
o = Construct.json_decode(obj.toString(), t);
} catch (MarshalException ex) {
throw ConfigRuntimeException.CreateUncatchableException(ex.getMessage(), t);
}
try {
return (Construct) o;
} catch (ClassCastException e) {
return CNull.NULL;
}
}
@Override
public Boolean runAsync() {
//Because we do IO
return true;
}
@Override
public LogLevel profileAt() {
return LogLevel.DEBUG;
}
}
@api(environments={GlobalEnv.class})
@noboilerplate
@seealso({com.laytonsmith.tools.docgen.templates.PersistenceNetwork.class})
public static class get_values extends AbstractFunction {
@Override
public String getName() {
return "get_values";
}
@Override
public Integer[] numArgs() {
return new Integer[]{Integer.MAX_VALUE};
}
@Override
public String docs() {
return "array {name[, space, ...]} Returns all the values in a particular namespace"
+ " as an associative"
+ " array(key: value, key: value). Only full namespace matches are considered,"
+ " so if the key 'users.data.username.hi' existed in the database, and you tried"
+ " get_values('users.data.user'), nothing would be returned. The last segment in"
+ " a key is also considered a namespace, so 'users.data.username.hi' would return"
+ " a single value (in this case).";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class, CREFormatException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return true;
}
@Override
public ExampleScript[] examples() throws ConfigCompileException {
return new ExampleScript[]{
new ExampleScript("Getting values", "store_value('x.top.a',true)\nstore_value('x.top.b',false)\nmsg(get_values('x'))"),
};
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
PersistenceNetwork p = environment.getEnv(GlobalEnv.class).GetPersistenceNetwork();
List<String> keyChain = new ArrayList<String>();
keyChain.add("storage");
String namespace = GetNamespace(args, null, getName(), t);
CHLog.GetLogger().Log(CHLog.Tags.PERSISTENCE, LogLevel.DEBUG, "Getting all values from " + namespace, t);
keyChain.addAll(Arrays.asList(namespace.split("\\.")));
Map<String[], String> list;
try {
list = p.getNamespace(keyChain.toArray(new String[keyChain.size()]));
} catch (DataSourceException ex) {
throw new CREIOException(ex.getMessage(), t, ex);
} catch(IllegalArgumentException e){
throw new CREFormatException(e.getMessage(), t, e);
}
CArray ca = CArray.GetAssociativeArray(t);
CHLog.GetLogger().Log(CHLog.Tags.PERSISTENCE, LogLevel.DEBUG, list.size() + " value(s) are being returned", t);
for (String[] e : list.keySet()) {
try {
String key = StringUtils.Join(e, ".").replaceFirst("storage\\.", ""); //Get that junk out of here
ca.set(new CString(key, t),
Construct.json_decode(list.get(e), t), t);
} catch (MarshalException ex) {
Logger.getLogger(Persistence.class.getName()).log(Level.SEVERE, null, ex);
}
}
return ca;
}
@Override
public CHVersion since() {
return CHVersion.V3_3_0;
}
@Override
public LogLevel profileAt() {
return LogLevel.DEBUG;
}
}
@api(environments={GlobalEnv.class})
@noboilerplate
@seealso({get_value.class, com.laytonsmith.tools.docgen.templates.PersistenceNetwork.class})
public static class has_value extends AbstractFunction {
@Override
public String getName() {
return "has_value";
}
@Override
public Integer[] numArgs() {
return new Integer[]{Integer.MAX_VALUE};
}
@Override
public String docs() {
return "boolean {[namespace, ...,] key} Returns whether or not there is data stored at the specified key in the Persistence database.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class, CREFormatException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public CHVersion since() {
return CHVersion.V3_1_2;
}
@Override
public Boolean runAsync() {
return true;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws ConfigRuntimeException {
try {
return CBoolean.get(env.getEnv(GlobalEnv.class).GetPersistenceNetwork().hasKey(("storage." + GetNamespace(args, null, getName(), t)).split("\\.")));
} catch (DataSourceException ex) {
throw new CREIOException(ex.getMessage(), t, ex);
} catch(IllegalArgumentException e){
throw new CREFormatException(e.getMessage(), t, e);
}
}
@Override
public LogLevel profileAt() {
return LogLevel.DEBUG;
}
}
@api(environments={GlobalEnv.class})
@noboilerplate
@seealso({store_value.class, com.laytonsmith.tools.docgen.templates.PersistenceNetwork.class})
public static class clear_value extends AbstractFunction {
@Override
public String getName() {
return "clear_value";
}
@Override
public Integer[] numArgs() {
return new Integer[]{Integer.MAX_VALUE};
}
@Override
public String docs() {
return "void {[namespace, ...,] key} Completely removes a value from storage. Calling has_value(key) after this call will return false.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREIOException.class, CREFormatException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public CHVersion since() {
return CHVersion.V3_3_0;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
String namespace = GetNamespace(args, null, getName(), t);
CHLog.GetLogger().Log(CHLog.Tags.PERSISTENCE, LogLevel.DEBUG, "Clearing value: " + namespace, t);
try {
environment.getEnv(GlobalEnv.class).GetPersistenceNetwork().clearKey(environment.getEnv(GlobalEnv.class).GetDaemonManager(), ("storage." + namespace).split("\\."));
} catch (DataSourceException ex) {
throw new CREIOException(ex.getMessage(), t, ex);
} catch (ReadOnlyException ex) {
throw new CREIOException(ex.getMessage(), t, ex);
} catch (IOException ex) {
throw new CREIOException(ex.getMessage(), t, ex);
} catch(IllegalArgumentException e){
throw new CREFormatException(e.getMessage(), t, e);
}
return CVoid.VOID;
}
@Override
public LogLevel profileAt() {
return LogLevel.DEBUG;
}
}
/**
* Generates the namespace for this value, given an array of constructs. If
* the entire list of arguments isn't supposed to be part of the namespace,
* the value to be excluded may be specified.
*
* @param args
* @param exclude
* @return
*/
private static String GetNamespace(Construct[] args, Integer exclude, String name, Target t) {
if (exclude != null && args.length < 2 || exclude == null && args.length < 1) {
throw new CREInsufficientArgumentsException(name + " was not provided with enough arguments. Check the documentation, and try again.", t);
}
boolean first = true;
StringBuilder b = new StringBuilder();
for (int i = 0; i < args.length; i++) {
if (exclude != null && exclude == i) {
continue;
}
if (!first) {
b.append(".");
}
first = false;
b.append(args[i].val());
}
return b.toString();
}
}