All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.labymedia.ultralight.databind.utils.JavascriptConversionUtils Maven / Gradle / Ivy

/*
 * Ultralight Java - Java wrapper for the Ultralight web engine
 * Copyright (C) 2020 - 2021 LabyMedia and contributors
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package com.labymedia.ultralight.databind.utils;

import com.labymedia.ultralight.databind.Databind;
import com.labymedia.ultralight.databind.DatabindJavascriptClass;
import com.labymedia.ultralight.javascript.JavascriptClass;
import com.labymedia.ultralight.javascript.JavascriptContext;
import com.labymedia.ultralight.javascript.JavascriptObject;
import com.labymedia.ultralight.javascript.JavascriptType;
import com.labymedia.ultralight.javascript.JavascriptValue;

import java.lang.reflect.Array;
import java.util.Date;

/**
 * Helper for converting between Java and Javascript objects and classes.
 */
public final class JavascriptConversionUtils {
    private final Databind databind;

    /**
     * Constructs a new {@link JavascriptConversionUtils} instance using the given {@link Databind} instance for
     * translating objects.
     *
     * @param databind The {@link Databind} instance to use
     */
    public JavascriptConversionUtils(Databind databind) {
        this.databind = databind;
    }

    /**
     * Converts a Java object to a Javascript object.
     *
     * @param context The Javascript context to use for the conversion
     * @param object  The Java object to convert
     * @return The converted object as a Javascript value
     */
    public JavascriptValue toJavascript(JavascriptContext context, Object object) {
        return toJavascript(context, object, object != null ? object.getClass() : null);
    }

    /**
     * Converts a Java object to a Javascript object.
     *
     * @param context   The Javascript context to use for the conversion
     * @param object    The Java object to convert
     * @param javaClass The target java class to convert to
     * @return The converted object as a Javascript value
     */
    public JavascriptValue toJavascript(JavascriptContext context, Object object, Class javaClass) {
        if (object == null || object == JavascriptType.NULL) {
            // Raw null
            return context.makeNull();
        } else if (object == JavascriptType.UNDEFINED) {
            // Raw undefined
            return context.makeUndefined();
        }

        javaClass = toWrapperClass(javaClass);

        // Decide based on the object's class
        if (javaClass == Boolean.class) {
            // Boolean conversion
            return context.makeBoolean((Boolean) object);
        } else if (Number.class.isAssignableFrom(javaClass)) {
            // All java integral types (except boolean and char) can be passed as doubles
            return context.makeNumber(((Number) object).doubleValue());
        } else if (javaClass == String.class) {
            // Strings are considered primitives in Javascript
            return context.makeString((String) object);
        } else if (javaClass.isArray()) {
            // Arrays required a recursive conversion
            // Reflective access to handle primitive arrays too
            int length = Array.getLength(object);
            JavascriptValue[] values = new JavascriptValue[length];

            for (int i = 0; i < length; i++) {
                // Recursive call
                Object value = Array.get(object, i);
                values[i] = toJavascript(context, value, value.getClass());
            }

            return context.makeArray(values);
        } else if (object instanceof Date) {
            // Dates are considered primitives in Javascript,
            // convert based on the unix epoch
            return context.makeDate(
                    context.makeNumber(((Date) object).getTime())
            );
        } else if (object instanceof JavascriptValue) {
            // Convert the object one-to-one
            JavascriptValue value = (JavascriptValue) object;
            if (value.isObject()) {
                // Cast the object if possible, or convert it to a Javascript object
                return value instanceof JavascriptObject ? (JavascriptObject) value : value.toObject();
            }

            return value;
        } else if (javaClass == Class.class) {
            // The base class needs special treatment
            return context.makeObject(
                    databind.toJavascript(javaClass, true),
                    new DatabindJavascriptClass.Data(null, javaClass));
        } else if (javaClass == JavascriptClass.class) {
            // The object is a javascript class already
            return context.makeObject((JavascriptClass) object, new DatabindJavascriptClass.Data(null, javaClass));
        }

        // Translate the object class
        return context.makeObject(databind.toJavascript(javaClass), new DatabindJavascriptClass.Data(object, javaClass));
    }

    /**
     * Converts a Javascript value to a Java object.
     *
     * @param value The Javascript value to convert
     * @param type  The type to convert the value to
     * @return The converted value
     */
    public Object fromJavascript(JavascriptValue value, Class type) {
        JavascriptType javascriptType = value.getType();

        if (type == JavascriptValue.class) {
            return value;
        } else if (type == JavascriptObject.class) {
            if (javascriptType != JavascriptType.OBJECT) {
                throw new IllegalArgumentException("Can not convert a non-object Javascript value to " + type.getName());
            }

            // Cast the object if possible or re-construct
            return value instanceof JavascriptObject ? (JavascriptObject) value : value.toObject();
        }

        if (javascriptType == JavascriptType.NULL || javascriptType == JavascriptType.UNDEFINED || type == void.class || type == Void.class || type == null) {
            // Special handling of Javascript null and undefined
            if (type != null && type.isPrimitive() && type != void.class) {
                // Primitives can not be null in Java
                throw new IllegalArgumentException(
                        "Can not convert " + (javascriptType == JavascriptType.NULL ? "null" : "undefined") + " to " + type.getName());
            }

            // Map Javascript null and undefined to Java null
            return null;
        }

        // Flatten the type to reduce checks
        type = toPrimitiveClass(type);

        if (javascriptType == JavascriptType.BOOLEAN) {
            // Simple boolean conversion
            if (type != boolean.class && type != Object.class) {
                throw new IllegalArgumentException("Can not convert Javascript boolean to " + type.getName());
            }

            // One-to-one mapping
            return value.toBoolean();
        } else if (javascriptType == JavascriptType.NUMBER) {
            // Number conversion
            Number number = value.toNumber();
            if (type == Object.class) {
                return number;
            }

            if (type == byte.class) {
                return number.byteValue();
            } else if (type == short.class) {
                return number.shortValue();
            } else if (type == int.class) {
                return number.intValue();
            } else if (type == long.class) {
                return number.longValue();
            } else if (type == float.class) {
                return number.floatValue();
            } else if (type == double.class) {
                return number.doubleValue();
            } else if (type == char.class) {
                return (char) number.shortValue();
            }

            throw new IllegalArgumentException("Can not convert Javascript number to " + type.getName());
        } else if (javascriptType == JavascriptType.STRING) {
            String str = value.toString();

            if (type.isAssignableFrom(String.class) || type == Object.class) {
                // String can be passed on
                return str;
            } else if (type == char[].class) {
                // Use the char[] directly
                return str.toCharArray();
            } else if (type == Character[].class) {
                // Convert char[] to Character[]
                char[] primitiveArray = str.toCharArray();
                Character[] objectArray = new Character[primitiveArray.length];

                for (int i = 0; i < primitiveArray.length; i++) {
                    objectArray[i] = primitiveArray[i];
                }

                return objectArray;
            }

            throw new IllegalArgumentException("Can not convert Javascript string to " + type.getName());
        } else if (javascriptType == JavascriptType.OBJECT) {
            JavascriptObject object = value.toObject();
            if (value.isDate()) {
                // Date's are primitives in Javascript
                if (!type.isAssignableFrom(Date.class)) {
                    throw new IllegalArgumentException("Can not convert Javascript date to " + type.getName());
                }

                // Convert based on the unix epoch with the help of the getTime method of Javascript
                long millis = (long) object.getProperty("getTime")
                        .toObject().callAsFunction(value.toObject()).toNumber();
                return new Date(millis);
            } else if (value.isArray()) {
                // The target might use any object, so just convert the JS array to an Object[]
                boolean anyType = type == Object.class;

                if (!type.isArray() && !anyType) {
                    throw new IllegalArgumentException("Can not convert a Javascript array to " + type.getName());
                }

                // Prepare an array reflectively
                int size = (int) object.getProperty("length").toNumber();
                Class componentType = anyType ? Object.class : type.getComponentType();
                Object objects = Array.newInstance(componentType, size);

                for (int i = 0; i < size; i++) {
                    // Recursively convert values
                    Array.set(objects, i, fromJavascript(object.getPropertyAtIndex(i), componentType));
                }

                return objects;
            } else if (databind.supportsFunctionalConversion() &&
                    object.isFunction() &&
                    type.isInterface() &&
                    type.isAnnotationPresent(FunctionalInterface.class)) {
                return FunctionalInterfaceBinder.bind(databind, type, object);
            }

            DatabindJavascriptClass.Data privateData = (DatabindJavascriptClass.Data) object.getPrivate();
            if (privateData == null) {
                // The Javascript object has not been constructed by java
                if (type == Object.class) {
                    return value;
                }

                throw new IllegalArgumentException(
                        "Can not convert a non Java constructed Javascript object to " + type.getName());
            }

            if (privateData.instance() == null) {
                if (!type.isAssignableFrom(Class.class) && type != Object.class) {
                    throw new IllegalArgumentException("Can not convert a Java class to " + type.getName());
                }

                return privateData.javaClass();
            } else {
                Object javaInstance = privateData.instance();
                if (!type.isAssignableFrom(javaInstance.getClass())) {
                    throw new IllegalArgumentException(
                            "Can not convert a " + javaInstance.getClass().getName() + " to " + type.getName());
                }

                return javaInstance;
            }
        }

        if (type == Object.class) {
            return value;
        }

        throw new IllegalArgumentException("Can not convert Javascript value to " + type.getName());
    }

    /**
     * Tries to infer the type from a Javascript value.
     *
     * @param value The value to infer the type from
     * @return The inferred type or null if the value is null or undefined
     */
    public static Class determineType(JavascriptValue value) {
        JavascriptType type = value.getType();

        if (type == JavascriptType.NULL || type == JavascriptType.UNDEFINED) {
            return null;
        }

        // Check primitives first
        if (type == JavascriptType.BOOLEAN) {
            return boolean.class;
        } else if (type == JavascriptType.NUMBER) {
            return Number.class;
        } else if (type == JavascriptType.STRING) {
            return String.class;
        } else if (type == JavascriptType.OBJECT) {
            // Value is an object, deep check
            JavascriptObject object = value.toObject();

            if (value.isDate()) {
                // Date's are primitives in Javascript
                return Date.class;
            } else if (value.isArray()) {
                int size = (int) object.getProperty("length").toNumber();

                JavascriptValue[] values = new JavascriptValue[size];

                for (int i = 0; i < size; i++) {
                    values[i] = object.getPropertyAtIndex(i);
                }

                // Create an array with the common superclass
                return Array.newInstance(findCommonSuperclass(values), 0).getClass();
            }

            if (object.getPrivate() == null) {
                // Not a Java object
                return JavascriptObject.class;
            }

            DatabindJavascriptClass.Data privateData = (DatabindJavascriptClass.Data) object.getPrivate();

            if (privateData.instance() == null) {
                // Java class
                return privateData.javaClass();
            } else {
                // Instance of a Java object
                return privateData.instance().getClass();
            }
        }

        throw new AssertionError("UNREACHABLE: Could not convert JavascriptValue to any java class");
    }

    /**
     * Finds the most common superclass of all values (interfaces are not taken into account).
     *
     * @param values The values to find the most common superclass of
     * @return The most common superclass
     */
    private static Class findCommonSuperclass(JavascriptValue... values) {
        Class[] classes = new Class[values.length];

        // Convert all Javascript values to Java classes
        for (int i = 0; i < classes.length; i++) {
            Class type = determineType(values[i]);
            classes[i] = type == null ? Object.class : type;
        }

        Class commonSuperclass = classes[0];

        // Find the most common superclass
        outer:
        for (; commonSuperclass != Object.class; commonSuperclass = commonSuperclass.getSuperclass()) {
            for (int i = 1; i < classes.length; i++) {
                if (!commonSuperclass.isAssignableFrom(classes[i])) {
                    continue outer;
                }
            }
            return commonSuperclass;
        }

        return commonSuperclass;
    }

    /**
     * Converts a Java class to its primitive variant if possible.
     *
     * @param source The class to convert
     * @return The primitive version of the class, or source, if no primitive version is available
     */
    private static Class toPrimitiveClass(Class source) {
        if (source.isPrimitive()) {
            return source;
        }

        if (source == Boolean.class) {
            return boolean.class;
        } else if (source == Byte.class) {
            return byte.class;
        } else if (source == Character.class) {
            return char.class;
        } else if (source == Short.class) {
            return short.class;
        } else if (source == Integer.class) {
            return int.class;
        } else if (source == Long.class) {
            return long.class;
        } else if (source == Float.class) {
            return float.class;
        } else if (source == Double.class) {
            return double.class;
        }

        return source;
    }

    /**
     * Converts a Java class to its wrapper variant if required.
     *
     * @param source The class to convert
     * @return The wrapper version of the class, or source, if no wrapper version exists
     */
    private static Class toWrapperClass(Class source) {
        if (!source.isPrimitive()) {
            return source;
        }

        if (source == boolean.class) {
            return Boolean.class;
        } else if (source == byte.class) {
            return Byte.class;
        } else if (source == char.class) {
            return Character.class;
        } else if (source == short.class) {
            return Short.class;
        } else if (source == int.class) {
            return Integer.class;
        } else if (source == long.class) {
            return Long.class;
        } else if (source == float.class) {
            return Float.class;
        } else if (source == double.class) {
            return Double.class;
        }

        throw new AssertionError("UNREACHABLE: Primitive class passed, but no wrapper class known");
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy