
org.stjs.javascript.JSAbstractOperations Maven / Gradle / Ivy
package org.stjs.javascript;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.text.DecimalFormat;
/**
* Implements various abstract operations as defined in the ECMA-262 specification. The visibility of this class is
* package private rather than public because the methods in this class are not intended to be called directly by user
* code, but should be used for implementing a Java runtime equivalent of the core JavaScript APIs.
*
*
* Note also that all the methods in this class are capitalized because the ECMA-262 specification defines them
* capitalized (probably to set them appart from normal functions). We have decided to follow their convention to make
* it clear when a real function is called and when an abstract operation is called.
*
* @author lordofthepigs
*/
class JSAbstractOperations {
private static final DecimalFormat PLAIN_INTEGER_FORMAT = new DecimalFormat("#0");
public static final BigInteger UINT_MAX_VALUE = new BigInteger("4294967296"); // = 2^32
public static final Double UINT_MAX_VALUE_D = 4294967296.0; // = 2^32
public static final BigInteger SINT_MAX_VALUE = new BigInteger("2147483648"); // = 2^31
public static final BigInteger USHORT_MAX_VALUE = new BigInteger("65536"); // = 2^16
/**
* The [[DefaultValue]] internal method of Object, as close as possible to the definition in the ECMA-262
* specification section 8.12.8
*
*
* This implementation of [[DefaultValue]] differs slightly from the ECMA-262 specification When applied to
* Objects whose class does not override the toString() or valueOf() methods. In this case, this
* implementation returns a String which is equal to whatever is produced by java.lang.Object.toString().
* Therefore, the returned string will not conform to the ECMA-262 specification which requires returning
* "[object Object]" in this case.
*
*
* This deviations is known and intentional. While it would have been possible to make the DefaultValue()
* implementation conform to ECMA-262, doing so would make it inconsistent with the result of the
* toString() method as defined by the java language. In order to minimize unexpected behavior,
* DefaultValue() behaves like toString() in those cases.
*
*
* Note that due to the fact that in Java toString() is guaranteed to return a String and is guaranteed to be
* callable, we cannot throw a TypeError from this method.
*/
static Object DefaultValue(Object arg) {
return DefaultValue(arg, null);
}
/**
* The [[DefaultValue]] internal method of Object, as close as possible to the definition in the ECMA-262
* specification section 8.12.8
*
*
*
* This implementation of [[DefaultValue]] differs slightly from the ECMA-262 specification When applied to
* Objects whose class does not override the toString() or valueOf() methods. In this case, this
* implementation returns a String which is equal to whatever is produced by java.lang.Object.toString().
* Therefore, the returned string will not conform to the ECMA-262 specification which requires returning
* "[object Object]" in this case.
*
*
* This deviations is known and intentional. While it would have been possible to make the DefaultValue()
* implementation conform to ECMA-262, doing so would make it inconsistent with the result of the
* toString() method as defined by the java language. In order to minimize unexpected behavior,
* DefaultValue() behaves like toString() in those cases.
*
*
* Note that due to the fact that in Java toString() is guaranteed to return a String and is guaranteed to be
* callable, we cannot throw a TypeError from this method.
*/
static Object DefaultValue(Object arg, String hint) {
if (hint == null) {
if (arg instanceof Date) {
return DefaultValue(arg, "String");
} else {
return DefaultValue(arg, "Number");
}
}
if (hint.equals("String")) {
try {
// note: no need to check if the return value is a primitive type as the spec specifies,
// because Java guarantees that we can only get a String, null or an exception
return callToString(arg);
}
catch (InvocationTargetException e) {
throw new Error("TypeError", e.getTargetException().getMessage(), e.getTargetException());
}
catch (Exception e) {
throw new Error("TypeError", e.getMessage(), e);
}
// toString is guaranteed to be callable and return a primitive value in Java, so we will
// never need to call valueOf();
} else if (hint.equals("Number")) {
if (isValueOfCallable(arg)) {
try {
Object value = callValueOf(arg);
if (isJsPrimitiveEquivalent(value)) {
return value;
}
}
catch (InvocationTargetException e) {
throw new Error("TypeError", e.getTargetException().getMessage(), e.getTargetException());
}
catch (Exception e) {
// we can still get exception like SecurityException or IllegalAccessException here
throw new Error("TypeError", e.getMessage(), e);
}
}
try {
// note: no need to check if the return value is a primitive type as the spec specifies,
// because Java guarantees that we can only get a String, null or an exception
return callToString(arg);
}
catch (InvocationTargetException e) {
throw new Error("TypeError", e.getTargetException().getMessage(), e.getTargetException());
}
catch (Exception e) {
throw new Error("TypeError", e.getMessage(), e);
}
}
// should not happen due to user code
throw new IllegalArgumentException("Unknown hint: " + hint);
}
private static String callToString(Object target) throws Exception {
Method toString = target.getClass().getMethod("toString");
if (toString.getDeclaringClass().equals(Object.class)) {
// replace the of the toString method of Object as defined by java (which doesn't match JS
// behavior) by the proper JS implementation
// TODO: actually replace by the proper JS implementation. This might not even be a good idea.
return (String) toString.invoke(target);
}
// some class in the hierarchy redefines toString(), so we can happily call it
return (String) toString.invoke(target);
}
private static boolean isValueOfCallable(Object target) {
try {
return target.getClass().getMethod("valueOf") != null;
}
catch (Exception e) {
return false;
}
}
private static Object callValueOf(Object target) throws Exception {
Method valueOf = target.getClass().getMethod("valueOf");
return valueOf.invoke(target);
}
private static boolean isJsPrimitiveEquivalent(Object arg) {
return arg == null || arg instanceof Boolean || arg instanceof String || arg instanceof Number;
}
/**
* The ToPrimitive() abstract operation, as close as possible to the definition in the ECMA-262 specification
* section 9.1.
*
*
* This implementation of ToPrimitive() differs slightly from the ECMA-262 specification When applied to
* Objects whose class does not override the toString() or valueOf() methods. In this case, this
* implementation returns a String which is equal to whatever is produced by java.lang.Object.toString().
* Therefore, the returned string will not conform to the ECMA-262 specification which requires returning
* "[object Object]" in this case.
*
*
* This deviations is known and intentional. While it would have been possible to make the ToPrimitive()()
* implementation conform to ECMA-262, doing so would make it inconsistent with the result of the
* toString() method as defined by the java language. In order to minimize unexpected behavior,
* ToPrimitive()() behaves like toString() in those cases.
*
*
* This method returns Objects even though the Specification says that the values should be JS primitives, but when
* actually used JS primitives really behave the same a their wrapper object (similar to auto-boxing in Java), so we
* can get away with returning the corresponding Object.
*/
static Object ToPrimitive(Object arg) {
return ToPrimitive(arg, "Number");
}
/**
* The ToPrimitive() abstract operation, as close as possible to the definition in the ECMA-262 specification
* section 9.1.
*
*
* This implementation of ToPrimitive() differs slightly from the ECMA-262 specification When applied to
* Objects whose class does not override the toString() or valueOf() methods. In this case, this
* implementation returns a String which is equal to whatever is produced by java.lang.Object.toString().
* Therefore, the returned string will not conform to the ECMA-262 specification which requires returning
* "[object Object]" in this case.
*
*
* This deviations is known and intentional. While it would have been possible to make the ToPrimitive()()
* implementation conform to ECMA-262, doing so would make it inconsistent with the result of the
* toString() method as defined by the java language. In order to minimize unexpected behavior,
* ToPrimitive()() behaves like toString() in those cases.
*
*
* This method returns Objects even though the Specification says that the values should be JS primitives, but when
* actually used JS primitives really behave the same a their wrapper object (similar to auto-boxing in Java), so we
* can get away with returning the corresponding Object.
*/
static Object ToPrimitive(Object arg, String hint) {
if (arg == null || arg instanceof Boolean || arg instanceof Number || arg instanceof String) {
return arg;
}
return DefaultValue(arg, hint);
}
/**
* The ToBoolean() abstract operation, as defined in the ECMA-262 specification section 9.2.
*/
static Boolean ToBoolean(Object arg) {
if (arg == null) {
return Boolean.FALSE;
} else if (arg instanceof Boolean) {
return (Boolean) arg;
} else if (arg instanceof Number) {
double value = ((Number) arg).doubleValue();
return !(Double.isNaN(value) || value == 0.0);
} else if (arg instanceof String) {
return !"".equals(arg);
}
return true;
}
/**
* The ToNumber() abstract operation, as defined in the ECMA-262 specification section 9.3.
*/
static Double ToNumber(Object arg) {
if (arg == null) {
return 0.0d;
} else if (Boolean.TRUE.equals(arg)) {
return 1.0d;
} else if (Boolean.FALSE.equals(arg)) {
return 0.0d;
} else if (arg instanceof String) {
return ToNumber((String) arg);
} else if (arg instanceof Number) {
return ((Number) arg).doubleValue();
}
Object primValue = ToPrimitive(arg, "Number");
return ToNumber(primValue).doubleValue();
}
/**
* The ToNumber() abstract operation applied to String, as defined in the ECMA-262 specification section 9.3.1
*/
static Double ToNumber(String arg) {
if (arg == null) {
return 0.0d;
}
String trimmed = arg.trim();
if (trimmed.isEmpty()) {
return 0.0d;
} else if ("Infinity".equals(arg)) {
return Double.POSITIVE_INFINITY;
}
if (trimmed.startsWith("0x") || trimmed.startsWith("0X")) {
// parse as a hexadecimal digit
try {
return Long.decode(trimmed).doubleValue();
}
catch (NumberFormatException e) {
return Double.NaN;
}
}
// parse as a decimal number
try {
return Double.parseDouble(trimmed);
}
catch (NumberFormatException e) {
return Double.NaN;
}
}
/**
* The ToInteger() abstract operation, as defined in the ECMA-262 specification section 9.4. Even though this
* operation is called ToInteger(), it still returns a Double, because it needs to be able to represent values such
* as NaN, +Infinity and -Infinity.
*/
static Double ToInteger(Object arg) {
Double number = ToNumber(arg);
if (number.isNaN()) {
return 0.0;
}
if (number.isInfinite()) {
return number;
}
return java.lang.Math.signum(number) * java.lang.Math.floor(java.lang.Math.abs(number));
}
/**
* The ToInt32() abstract operation, as defined in the ECMA-262 specification section 9.5
*/
static Double ToInt32(Object arg) {
Double number = ToNumber(arg);
if (number.isNaN() || number.isInfinite() || number == 0.0) {
return 0.0;
}
Double posInt = ToInteger(number);
// here we have to use BigInteger, because posInt could easily be waaay out of range for int
// and the spec specifies how to handle that case
BigInteger posIntBig = new BigInteger(PLAIN_INTEGER_FORMAT.format(posInt));
BigInteger int32bit = posIntBig.mod(UINT_MAX_VALUE);
if (int32bit.compareTo(SINT_MAX_VALUE) >= 0) {
return int32bit.subtract(UINT_MAX_VALUE).doubleValue();
}
return int32bit.doubleValue();
}
/**
* The ToUInt32() abstract operation, as defined in the ECMA-262 specification section 9.6
*/
static Double ToUInt32(Object arg) {
Double number = ToNumber(arg);
if (number.isNaN() || number.isInfinite() || number == 0.0) {
return 0.0;
}
Double posInt = ToInteger(number);
// here we have to use BigInteger, because posInt could easily be waaay out of range for int
// and the spec specifies how to handle that case
BigInteger posIntBig = new BigInteger(PLAIN_INTEGER_FORMAT.format(posInt));
BigInteger int32bit = posIntBig.mod(UINT_MAX_VALUE);
return int32bit.doubleValue();
}
/**
* The ToUInt16() abstract operation, as defined in the ECMA-262 specification section 9.7
*/
static Double ToUInt16(Object arg) {
Double number = ToNumber(arg);
if (number.isNaN() || number.isInfinite() || number == 0.0) {
return 0.0;
}
Double posInt = ToInteger(number);
// here we have to use BigInteger, because posInt could easily be waaay out of range for int
// and the spec specifies how to handle that case
BigInteger posIntBig = new BigInteger(PLAIN_INTEGER_FORMAT.format(posInt));
BigInteger int16bit = posIntBig.mod(USHORT_MAX_VALUE);
return int16bit.doubleValue();
}
/**
* The ToString() abstract operation, as close as possible to the definition in the ECMA-262 specification
* section 9.8.
*
*
* This implementation of ToString() differs slightly from the ECMA-262 specification in the following
* manner:
*
* - When ToString() is applied to Objects whose class does not override the toString() method,
* then the returned string is equal to whatever is produced by java.lang.Object.toString(). Therefore, the
* returned string will not conform to the ECMA-262 specification which requires returning
* "[object Object]" in this case.
*
- ToString(0) returns "0" and to ToString(0.0) return "0.0", which are not
* equal. The ECMA-262 specification mentions a very specific algorithm for converting numbers to string, which due
* to the difference between the various subclasses of java.lang.Number is not fully respected by this
* implementation
*
*
* Both of these deviations are known and intentional. While it would have been possible to make the
* ToString() operation conform to ECMA-262, doing so would make it inconsistent with the result of the
* toString() method as defined by the java language. In order to minimize unexpected behavior,
* ToString() behaves like toString() in those cases.
*/
static String ToString(Object arg) {
if (arg == null) {
return "null";
} else if (Boolean.TRUE.equals(arg)) {
return "true";
} else if (Boolean.FALSE.equals(arg)) {
return "false";
} else if (arg instanceof String) {
return (String) arg;
} else if (arg instanceof Number) {
return ToString((Number) arg);
}
Object primValue = ToPrimitive(arg, "String");
return ToString(primValue);
}
/**
* The ToString() abstract operation for Numbers, as close as possible to the definition in the ECMA-262
* specification section 9.8.1.
*
*
* This implementation of ToString() differs slightly from the ECMA-262 specification. ToString(0)
* returns "0" and to ToString(0.0) return "0.0", which are not equal. The ECMA-262
* specification mentions a very specific algorithm for converting numbers to string, which due to the difference
* between the various subclasses of java.lang.Number is not fully respected by this implementation
*
* This deviation is known and intentional. While it would have been possible to make the ToString()
* operation conform to ECMA-262, doing so would make it inconsistent with the result of the toString()
* method as defined by the java language. In order to minimize unexpected behavior, ToString() behaves
* like toString() in those cases.
*/
static String ToString(Number arg) {
if (arg == null) {
return "null";
}
return arg.toString();
}
/**
* The ToObject() operation, as defined in the ECMA-262 specification section 9.9
*/
static Object ToObject(Object arg) {
if (arg == null) {
throw new Error("TypeError", "null is not an Object");
}
return arg;
}
/**
* The CheckObjectCoercible operation, as defined in the ECMA-262 specification section 9.10
*/
static boolean CheckObjectCoercible(Object arg) {
return arg != null;
}
}