package com.laytonsmith.core; import com.laytonsmith.annotations.typeof; import com.laytonsmith.core.constructs.*; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.CRE.CRERangeException; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.core.natives.interfaces.ArrayAccess; import java.util.regex.Pattern; /** * This class provides a way to validate, parse, and manipulate arguments passed * to functions in a standard, minimalist way, ultimately retrieving java * objects from the arguments. Many of these methods were originally from the * Static class, but have been moved into this class, which better groups them * together. */ public class ArgumentValidation { private ArgumentValidation() { // } /** * Returns an item from an array, as a generic construct. This provides a * standard way of returning an item from an array. If defaultItem is null, * then it is required that the item be present in the object. If it is not, * a {@link ConfigRuntimeException} is thrown. * * @param object The array to look in. * @param key The key to search for * @param t The code target * @param defaultItem The default item to return if the specified key isn't * present in the array. If this is a java null, and the key isn't present, * a standard error message is thrown. * @return The item in the array, or the defaultItem. * @throws ConfigRuntimeException A FormatException is thrown if it doesn't * contain the appropriate value and the defaultItem is null. */ public static Construct getItemFromArray(CArray object, String key, Target t, Construct defaultItem) throws ConfigRuntimeException { if (object.containsKey(key)) { return object.get(key, t); } else if (defaultItem == null) { throw new CREFormatException("Expected the key \"" + key + "\" to be present, but it was not found.", t); } else { return defaultItem; } } /** * Returns a CArray object from a given construct, throwing a common error * message if not. * * @param construct * @param t * @return */ public static CArray getArray(Construct construct, Target t) { if (construct instanceof CArray) { return ((CArray) construct); } else { throw new CRECastException("Expecting array, but received " + construct.val(), t); } } /** * Works like the other get* methods, but works in a more generic way for * other types of Constructs. * * @param <T> The type expected. * @param construct The generic object * @param t Code target * @param expectedClassName The expected class type, for use in the error * message if the construct is the wrong type. * @param clazz The type expected. * @return The properly cast object. * @deprecated Use * {@link #getObject(com.laytonsmith.core.constructs.Construct, com.laytonsmith.core.constructs.Target, java.lang.Class)} * instead, as that gets the expected class name automatically. */ @Deprecated public static <T extends Construct> T getObject(Construct construct, Target t, String expectedClassName, Class<T> clazz) { if (clazz.isAssignableFrom(construct.getClass())) { return (T) construct; } else { throw new CRECastException("Expecting " + expectedClassName + " but receieved " + construct.val() + " instead.", t); } } /** * Works like the other get* methods, but works in a more generic way for * other types of Constructs. It also assumes that the class specified is * tagged with a typeof annotation, thereby preventing the need for the * expectedClassName like the deprecated version uses. * * This will work if the value is a subtype of the expected value. * * @param <T> The type expected. * @param construct The generic object * @param t Code target * @param clazz The type expected. * @return The properly cast object. */ public static <T extends Construct> T getObject(Construct construct, Target t, Class<T> clazz) { if (clazz.isAssignableFrom(construct.getClass())) { return (T) construct; } else { String expectedClassName = clazz.getAnnotation(typeof.class).value(); String actualClassName = construct.getClass().getAnnotation(typeof.class).value(); throw new CRECastException("Expecting " + expectedClassName + " but receieved " + construct.val() + " (" + actualClassName + ") instead.", t); } } /** * This function pulls a numerical equivalent from any given construct. It * throws a ConfigRuntimeException if it cannot be converted, for instance * the string "s" cannot be cast to a number. The number returned will * always be a double. * * @param c * @param t * @return */ public static double getNumber(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } double d; if (c == null || c instanceof CNull) { return 0.0; } if (c instanceof CInt) { d = ((CInt) c).getInt(); } else if (c instanceof CDouble) { d = ((CDouble) c).getDouble(); } else if (c instanceof CString) { try { d = Double.parseDouble(c.val()); } catch (NumberFormatException e) { throw new CRECastException("Expecting a number, but received \"" + c.val() + "\" instead", t); } } else if (c instanceof CBoolean) { if (((CBoolean) c).getBoolean()) { d = 1; } else { d = 0; } } else { throw new CRECastException("Expecting a number, but received \"" + c.val() + "\" instead", t); } return d; } // Matches a string that will be successfully parsed by Double.parseDouble(String) // Based on https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html#valueOf-java.lang.String- private static final Pattern VALID_DOUBLE = Pattern.compile( "[\\x00-\\x20]*" + // leading whitespace "[+-]?(" + "(" + "((\\p{Digit}+)(\\.)?((\\p{Digit}+)?)([eE][+-]?(\\p{Digit}+))?)" + "|" + "(\\.(\\p{Digit}+)([eE][+-]?(\\p{Digit}+))?)" + "|" + // Hexadecimal strings "((" + "(0[xX](\\p{XDigit}+)(\\.)?)" + "|" + "(0[xX](\\p{XDigit}+)?(\\.)(\\p{XDigit}+))" + ")[pP][+-]?(\\p{Digit}+))" + ")[fFdD]?" + ")[\\x00-\\x20]*" // trailing whitespace ); /** * Validates that a construct's value is a number or string that can be returned by GetNumber() * * @param c Construct * @return boolean */ public static boolean isNumber(Construct c) { return c instanceof CNumber || VALID_DOUBLE.matcher(c.val()).matches(); } /** * Alias to getNumber * * @param c * @param t * @return */ public static double getDouble(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } try { return getNumber(c, t); } catch (ConfigRuntimeException e) { throw new CRECastException("Expecting a double, but received " + c.val() + " instead", t); } } /** * Returns a 32 bit float from the construct. Since the backing value is * actually a double, if the number contained in the construct is not the same * after truncating, an exception is thrown (fail fast). When needing an float * from a construct, this method is much preferred over silently truncating. * * @param c * @param t * @return */ public static float getDouble32(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } // Use 6 places at most else the imprecisions of float makes this function throw the exception. double delta = 0.0000001; double l = getDouble(c, t); float f = (float) l; if (Math.abs(f - l) > delta) { throw new CRERangeException("Expecting a 32 bit float, but a larger value was found: " + l, t); } return f; } /** * Returns a long from any given construct. * * @param c * @param t * @return */ public static long getInt(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } long i; if (c == null || c instanceof CNull) { return 0; } if (c instanceof CInt) { i = ((CInt) c).getInt(); } else if (c instanceof CBoolean) { if (((CBoolean) c).getBoolean()) { i = 1; } else { i = 0; } } else { try { i = Long.parseLong(c.val()); } catch (NumberFormatException e) { throw new CRECastException("Expecting an integer, but received \"" + c.val() + "\" instead", t); } } return i; } /** * Returns a 32 bit int from the construct. Since the backing value is * actually a long, if the number contained in the construct is not the same * after truncating, an exception is thrown (fail fast). When needing an int * from a construct, this method is much preferred over silently truncating. * * @param c * @param t * @return */ public static int getInt32(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } long l = getInt(c, t); int i = (int) l; if (i != l) { throw new CRERangeException("Expecting a 32 bit integer, but a larger value was found: " + l, t); } return i; } /** * Returns a 16 bit int from the construct (a short). Since the backing * value is actually a long, if the number contained in the construct is not * the same after truncating, an exception is thrown (fail fast). When * needing an short from a construct, this method is much preferred over * silently truncating. * * @param c * @param t * @return */ public static short getInt16(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } long l = getInt(c, t); short s = (short) l; if (s != l) { throw new CRERangeException("Expecting a 16 bit integer, but a larger value was found: " + l, t); } return s; } /** * Returns an 8 bit int from the construct (a byte). Since the backing value * is actually a long, if the number contained in the construct is not the * same after truncating, an exception is thrown (fail fast). When needing a * byte from a construct, this method is much preferred over silently * truncating. * * @param c * @param t * @return */ public static byte getInt8(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } long l = getInt(c, t); byte b = (byte) l; if (b != l) { throw new CRERangeException("Expecting an 8 bit integer, but a larger value was found: " + l, t); } return b; } /** * Returns a boolean from any given construct. Depending on the type of the * construct being converted, it follows the following rules: If it is an * integer or a double, it is false if 0, true otherwise. If it is a string, array, * or other ArrayAccess value, if it is empty, it is false, otherwise it is true. * * @param c * @param t * @return */ public static boolean getBoolean(Construct c, Target t) { if(c instanceof CMutablePrimitive){ c = ((CMutablePrimitive)c).get(); } boolean b = false; if (c == null) { return false; } if (c instanceof CBoolean) { b = ((CBoolean) c).getBoolean(); } else if (c instanceof CString) { b = (c.val().length() > 0); } else if (c instanceof CInt || c instanceof CDouble) { b = !(getNumber(c, t) == 0); } else if (c instanceof ArrayAccess) { b = !(((ArrayAccess) c).size() == 0); } return b; } /** * Returns a CByteArray object from the given construct. * @param c * @param t * @return */ public static CByteArray getByteArray(Construct c, Target t) { if (c instanceof CByteArray) { return (CByteArray) c; } else if (c instanceof CNull) { return new CByteArray(t, 0); } else { throw new CRECastException("Expecting byte array, but found " + c.typeof() + " instead.", t); } } public static CClassType getClassType(Construct c, Target t){ if(c instanceof CClassType){ return (CClassType) c; } else { throw new CRECastException("Expecting a ClassType, but found " + c.typeof() + " instead.", t); } } /** * Returns a String object from the given construct. Note that no * validation is done, because all Constructs can be toString'd, but * this method is provided for consistency sake. * @param c * @param t * @return */ public static String getString(Construct c, Target t){ return c.val(); } /** * Returns true if any of the constructs are a CDouble, false otherwise. * * @param c * @return */ public static boolean anyDoubles(Construct... c) { for (Construct c1 : c) { if (c1 instanceof CDouble) { return true; } } return false; } /** * Return true if any of the constructs are CStrings, false otherwise. * * @param c * @return */ public static boolean anyStrings(Construct... c) { for (Construct c1 : c) { if (c1 instanceof CString) { return true; } } return false; } /** * Returns true if any of the constructs are null * @param c * @return */ public static boolean anyNulls(Construct... c){ for(Construct c1 : c){ if(c1 instanceof CNull){ return true; } } return false; } /** * Returns true if any of the constructs are CBooleans, false otherwise. * * @param c * @return */ public static boolean anyBooleans(Construct... c) { for (Construct c1 : c) { if (c1 instanceof CBoolean) { return true; } } return false; } }