package com.laytonsmith.core.constructs;
import com.laytonsmith.PureUtilities.Version;
import com.laytonsmith.annotations.typeof;
import com.laytonsmith.core.SimpleDocumentation;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.exceptions.MarshalException;
import com.laytonsmith.core.natives.interfaces.Mixed;
import java.io.File;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
/**
*
*
*/
public abstract class Construct implements Cloneable, Comparable<Construct>, Mixed {
public enum ConstructType {
TOKEN, COMMAND, FUNCTION, VARIABLE, LITERAL, ARRAY, MAP, ENTRY, INT,
DOUBLE, BOOLEAN, NULL, STRING, VOID, IVARIABLE, CLOSURE, LABEL, SLICE,
SYMBOL, IDENTIFIER, BRACE, BRACKET, BYTE_ARRAY, RESOURCE, LOCK, MUTABLE_PRIMITIVE,
CLASS_TYPE;
}
private final ConstructType ctype;
private final String value;
private Target target;
private transient boolean wasIdentifier = false;
public ConstructType getCType() {
return ctype;
}
/**
* This method should only be used by Script when setting the children's
* target, if it's an ivariable.
*
* @param target
*/
public void setTarget(Target target) {
this.target = target;
}
public final String getValue() {
return val();
}
public int getLineNum() {
return target.line();
}
public File getFile() {
return target.file();
}
public int getColumn() {
return target.col();
}
public Target getTarget() {
return target;
}
public Construct(String value, ConstructType ctype, int line_num, File file, int column) {
this.value = value;
Static.AssertNonNull(value, "The string value may not be null.");
this.ctype = ctype;
this.target = new Target(line_num, file, column);
}
public Construct(String value, ConstructType ctype, Target t) {
this.value = value;
Static.AssertNonNull(value, "The string value may not be null.");
this.ctype = ctype;
this.target = t;
}
/**
* Returns the standard string representation of this Construct. This will
* never return null.
*
* @return
*/
@Override
public String val() {
return value;
}
public void setWasIdentifier(boolean b) {
wasIdentifier = b;
}
public boolean wasIdentifier() {
return wasIdentifier;
}
/**
* Returns the standard string representation of this Construct, except in
* the case that the construct is a CNull, in which case it returns java
* null.
*
* @return
*/
public String nval() {
return val();
}
@Override
public String toString() {
return value;
}
@Override
public Construct clone() throws CloneNotSupportedException {
return (Construct) super.clone();
}
/**
* This function takes a Construct, and turns it into a JSON value. If the
* construct is not one of the following, a MarshalException is thrown:
* CArray, CBoolean, CDouble, CInt, CNull, CString, CVoid, Command.
* Currently unsupported, but will be in the future are: CClosure/CFunction
* The following map is applied when encoding and decoding:
* <table border='1'>
* <tr><th>JSON</th><th>MethodScript</th></tr>
* <tr><td>string</td><td>CString, CVoid, Command, but all are decoded into
* CString</td></tr>
* <tr><td>number</td><td>CInt, CDouble, and it is decoded
* intelligently</td></tr>
* <tr><td>boolean</td><td>CBoolean</td></tr>
* <tr><td>null</td><td>CNull</td></tr>
* <tr><td>array/object</td><td>CArray</td></tr>
* </table>
*
* @param c
* @return
*/
public static String json_encode(Construct c, Target t) throws MarshalException {
return JSONValue.toJSONString(json_encode0(c, t));
}
private static Object json_encode0(Construct c, Target t) throws MarshalException {
if (c instanceof CString || c instanceof Command) {
return c.val();
} else if (c instanceof CVoid) {
return "";
} else if (c instanceof CInt) {
return ((CInt) c).getInt();
} else if (c instanceof CDouble) {
return ((CDouble) c).getDouble();
} else if (c instanceof CBoolean) {
return ((CBoolean) c).getBoolean();
} else if (c instanceof CNull) {
return null;
} else if (c instanceof CArray) {
CArray ca = (CArray) c;
if (!ca.inAssociativeMode()) {
List<Object> list = new ArrayList<Object>();
for (int i = 0; i < ca.size(); i++) {
list.add(json_encode0(ca.get(i, t), t));
}
return list;
} else {
Map<String, Object> map = new HashMap<String, Object>();
for (String key : ca.stringKeySet()) {
map.put(key, json_encode0(ca.get(key, t), t));
}
return map;
}
} else {
throw new MarshalException("The type of " + c.getClass().getSimpleName() + " is not currently supported", c);
}
}
/**
* Takes a string and converts it into a Construct
*
* @param s
* @return
*/
public static Construct json_decode(String s, Target t) throws MarshalException {
if (s == null) {
return CNull.NULL;
}
if ("".equals(s.trim())) {
throw new MarshalException();
}
if (s.startsWith("{")) {
//Object
JSONObject obj = (JSONObject) JSONValue.parse(s);
CArray ca = CArray.GetAssociativeArray(t);
if (obj == null) {
//From what I can tell, this happens when the json object is improperly formatted,
//so go ahead and throw an exception
throw new MarshalException();
}
for (Object key : obj.keySet()) {
ca.set(convertJSON(key, t),
convertJSON(obj.get(key), t), t);
}
return ca;
} else if (s.startsWith("[")) {
//It's an array
JSONArray array = (JSONArray) JSONValue.parse(s);
if (array == null) {
throw new MarshalException();
}
CArray carray = new CArray(t);
for (int i = 0; i < array.size(); i++) {
carray.push(convertJSON(array.get(i), t), t);
}
return carray;
} else {
//It's a single value, but we're gonna wrap it in an array, then deconstruct it
s = "[" + s + "]";
JSONArray array = (JSONArray) JSONValue.parse(s);
if (array == null) {
//It's a null value
return CNull.NULL;
}
Object o = array.get(0);
return convertJSON(o, t);
}
}
private static Construct convertJSON(Object o, Target t) throws MarshalException {
if (o instanceof String) {
return new CString((String) o, Target.UNKNOWN);
} else if (o instanceof Number) {
Number n = (Number) o;
if (n.longValue() == n.doubleValue()) {
//It's an int
return new CInt(n.longValue(), Target.UNKNOWN);
} else {
//It's a double
return new CDouble(n.doubleValue(), Target.UNKNOWN);
}
} else if (o instanceof Boolean) {
return CBoolean.get((Boolean) o);
} else if (o instanceof java.util.List) {
java.util.List l = (java.util.List) o;
CArray ca = new CArray(t);
for (Object l1 : l) {
ca.push(convertJSON(l1, t), t);
}
return ca;
} else if (o == null) {
return CNull.NULL;
} else if (o instanceof java.util.Map) {
CArray ca = CArray.GetAssociativeArray(t);
for (Object key : ((java.util.Map) o).keySet()) {
ca.set(convertJSON(key, t),
convertJSON(((java.util.Map) o).get(key), t), t);
}
return ca;
} else {
throw new MarshalException(o.getClass().getSimpleName() + " are not currently supported");
}
}
@Override
public int compareTo(Construct c) {
if (this.value.contains(" ") || this.value.contains("\t")
|| c.value.contains(" ") || c.value.contains("\t")) {
return this.value.compareTo(c.value);
}
try {
Double d1 = Double.valueOf(this.value);
Double d2 = Double.valueOf(c.value);
return d1.compareTo(d2);
} catch (NumberFormatException e) {
return this.value.compareTo(c.value);
}
}
/**
* Converts a POJO to a Construct, if the type is convertable. This accepts
* many types of objects, and should be expanded if a type does fit into the
* overall type scheme.
*
* @param o
* @return
* @throws ClassCastException
*/
public static Construct GetConstruct(Object o) throws ClassCastException {
return Construct.GetConstruct(o, false);
}
/**
* Converts a POJO to a Construct, if the type is convertable. This accepts
* many types of objects, and should be expanded if a type does fit into the
* overall type scheme.
*
* @param o
* @param allowResources If true, unknown objects will be converted to a CResource.
* @return
* @throws ClassCastException
*/
public static Construct GetConstruct(Object o, boolean allowResources) throws ClassCastException {
if (o == null) {
return CNull.NULL;
} else if (o instanceof CharSequence) {
return new CString((CharSequence) o, Target.UNKNOWN);
} else if (o instanceof Number) {
if (o instanceof Integer || o instanceof Long || o instanceof Byte || o instanceof BigInteger
|| o instanceof AtomicInteger || o instanceof Short) {
//integral
return new CInt(((Number) o).longValue(), Target.UNKNOWN);
} else {
//floating point
return new CDouble(((Number) o).doubleValue(), Target.UNKNOWN);
}
} else if (o instanceof Boolean) {
return CBoolean.get((Boolean) o);
} else if (o instanceof Map) {
//associative array
CArray a = CArray.GetAssociativeArray(Target.UNKNOWN);
Map m = (Map) o;
for (Object key : m.keySet()) {
a.set(key.toString(), GetConstruct(m.get(key), allowResources), Target.UNKNOWN);
}
return a;
} else if (o instanceof Collection) {
//normal array
CArray a = new CArray(Target.UNKNOWN);
Collection l = (Collection) o;
for (Object obj : l) {
a.push(GetConstruct(obj, allowResources), Target.UNKNOWN);
}
return a;
} else {
throw new ClassCastException(o.getClass().getName() + " cannot be cast to a Construct type");
}
}
/**
* Converts a Construct to a POJO, if the type is convertable. The types
* returned from this method are set, unlike GetConstruct which is more
* flexible. The mapping is precisely as follows:
* <ul>
* <li>boolean -> Boolean</li>
* <li>integer -> Long</li>
* <li>double -> Double</li>
* <li>string -> String</li>
* <li>normal array -> ArrayList<Object></li>
* <li>associative array -> SortedMap<String, Object></li>
* <li>null -> null</li>
* <li>resource -> Object</li>
* </ul>
*
* @param c
* @return
* @throws ClassCastException
*/
public static Object GetPOJO(Construct c) throws ClassCastException {
if (c instanceof CNull) {
return null;
} else if (c instanceof CString) {
return c.val();
} else if (c instanceof CBoolean) {
return Boolean.valueOf(((CBoolean) c).getBoolean());
} else if (c instanceof CInt) {
return Long.valueOf(((CInt) c).getInt());
} else if (c instanceof CDouble) {
return Double.valueOf(((CDouble) c).getDouble());
} else if (c instanceof CArray) {
CArray ca = (CArray) c;
if (ca.inAssociativeMode()) {
//SortedMap
SortedMap<String, Object> map = new TreeMap<>();
for (String key : ca.stringKeySet()) {
map.put(key, GetPOJO(ca.get(key, Target.UNKNOWN)));
}
return map;
} else {
//ArrayList
ArrayList<Object> list = new ArrayList<Object>((int) ca.size());
for (int i = 0; i < ca.size(); i++) {
list.add(GetPOJO(ca.get(i, Target.UNKNOWN)));
}
return list;
}
} else if (c instanceof CResource) {
return ((CResource) c).getResource();
} else {
throw new ClassCastException(c.getClass().getName() + " cannot be cast to a POJO");
}
}
public CString asString() {
return new CString(val(), target);
}
/**
* If this type of construct is dynamic, that is to say, if it isn't a
* constant. Things like 9, and 's' are constant. Things like {@code @value}
* are dynamic.
*
* @return
*/
public abstract boolean isDynamic();
/**
* Returns the underlying value, as a value that can be directly inserted
* into code. So, if the value were {@code This is 'the value'}, then
* {@code 'This is \'the value\''} would be returned. (That is, characters
* needing escapes will be escaped.) It includes the outer quotes as well.
* Numbers and other primitives may be able to override this to return a
* valid value as well. By default, this assumes a string, and returns
* appropriately.
*
* @return
*/
protected String getQuote() {
return "'" + val().replace("\\", "\\\\").replace("'", "\\'") + "'";
}
/**
* Returns the typeof this Construct, as a string. Not all constructs are
* annotated with the @typeof annotation, in which case this is considered a
* "private" object, which can't be directly accessed via MethodScript. In
* this case, an IllegalArgumentException is thrown.
*
* @return
* @throws IllegalArgumentException If the class isn't public facing.
*/
public final String typeof() {
typeof ann = this.getClass().getAnnotation(typeof.class);
if (ann == null) {
throw new IllegalArgumentException();
}
return ann.value();
}
/**
* Overridden from {@link SimpleDocumentation}. This should just return the value
* of the typeof annotation, unconditionally.
* @return
*/
@Override
public final String getName() {
typeof t = this.getClass().getAnnotation(typeof.class);
return t.value();
}
// We provide default instances of these methods, though they should in practice never run.
@Override
public String docs() {
throw new UnsupportedOperationException();
}
@Override
public Version since() {
throw new UnsupportedOperationException();
}
}