reactor.js.core.JavaScriptObject.java Source code

Java tutorial

Introduction

Here is the source code for reactor.js.core.JavaScriptObject.java

Source

/*
 * Copyright (c) 2011-2014 GoPivotal, Inc. All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package reactor.js.core;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.gs.collections.api.list.ImmutableList;
import com.gs.collections.api.map.MutableMap;
import com.gs.collections.api.set.MutableSet;
import com.gs.collections.impl.list.mutable.FastList;
import com.gs.collections.impl.map.mutable.SynchronizedMutableMap;
import com.gs.collections.impl.map.mutable.UnifiedMap;
import com.gs.collections.impl.set.mutable.UnifiedSet;
import jdk.nashorn.api.scripting.AbstractJSObject;
import jdk.nashorn.api.scripting.JSObject;
import jdk.nashorn.internal.runtime.ScriptFunction;
import jdk.nashorn.internal.runtime.ScriptObject;
import jdk.nashorn.internal.runtime.ScriptRuntime;
import org.jetbrains.annotations.NotNull;
import reactor.alloc.Recyclable;
import reactor.function.Supplier;

import javax.script.Bindings;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;

/**
 * @author Jon Brisbin
 */
public class JavaScriptObject extends AbstractJSObject implements Bindings, Recyclable, Cloneable {

    private static final MutableMap<Class<?>, Function<Object, Object>> FIELD_ACCESSORS = SynchronizedMutableMap
            .of(UnifiedMap.newMap());
    private static final Supplier<?> NULL_SUPPLIER = () -> null;

    private enum ParentType {
        JSOBJECT, SCRIPTOBJECT, MAP, BEAN;

        public Object get(Object obj, String key) {
            switch (this) {
            case JSOBJECT:
                return ((JSObject) obj).getMember(key);
            case SCRIPTOBJECT:
                return ((ScriptObject) obj).get(key);
            case MAP:
                return ((Map) obj).get(key);
            case BEAN:
                return FIELD_ACCESSORS.getIfAbsentPut(obj.getClass(), () -> new FieldAccessor(obj, key)).apply(key);
            default:
                return null;
            }
        }

        public static ParentType of(Object obj) {
            if (obj instanceof JSObject) {
                return ParentType.JSOBJECT;
            } else if (obj instanceof ScriptObject) {
                return ParentType.SCRIPTOBJECT;
            } else if (obj instanceof Map) {
                return ParentType.MAP;
            } else {
                return ParentType.BEAN;
            }
        }
    }

    private final MutableMap<String, Object> properties;
    private final MutableMap<String, Supplier<?>> propertySuppliers;
    private final ImmutableList<String> readOnlyProperties;

    private volatile Object parent;
    private volatile ParentType parentType;

    public JavaScriptObject() {
        this(null, null, null, null);
    }

    public JavaScriptObject(Map<String, Object> properties, Map<String, Supplier<?>> propertySuppliers,
            List<String> readOnlyProperties, Object parent) {
        if (null == properties) {
            properties = Collections.emptyMap();
        }
        if (null == propertySuppliers) {
            propertySuppliers = Collections.emptyMap();
        }
        if (null == readOnlyProperties) {
            readOnlyProperties = Collections.emptyList();
        }
        this.properties = UnifiedMap.newMap(properties);
        this.propertySuppliers = UnifiedMap.newMap(propertySuppliers);
        this.readOnlyProperties = FastList.newList(readOnlyProperties).toImmutable();
        setParent(parent);
    }

    public void setParent(Object parent) {
        this.parent = parent;
        this.parentType = ParentType.of(parent);
    }

    @Override
    public Object call(Object thiz, Object... args) {
        if (!hasProperty("apply")) {
            throw new NoSuchMethodError(this + " does not have a property 'apply' so cannot be invoked.");
        }
        Object apply = getProperty("apply");
        if (apply instanceof ScriptFunction) {
            return ScriptRuntime.apply((ScriptFunction) apply, thiz, args);
        } else if (apply instanceof JSObject) {
            return ((JSObject) apply).call(thiz, args);
        } else {
            throw new IllegalArgumentException("Cannot invoke an instance of " + apply);
        }
    }

    public Map<String, Object> getProperties() {
        return properties;
    }

    public Object newObject(Object... args) {
        return new JavaScriptObject(properties, propertySuppliers, readOnlyProperties.castToList(), this);
    }

    public <T> JavaScriptObject supply(String name, Supplier<T> supplier) {
        propertySuppliers.put(name, supplier);
        return this;
    }

    @JsonAnySetter
    @Override
    public Object put(String name, Object value) {
        if (readOnlyProperties.contains(name)) {
            throw new IllegalArgumentException("Property '" + name + "' is read-only");
        }
        return properties.put(name, value);
    }

    @Override
    public void putAll(Map<? extends String, ? extends Object> toMerge) {
        for (Entry<? extends String, ? extends Object> entry : toMerge.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public boolean containsKey(Object key) {
        return hasProperty(key);
    }

    @JsonAnyGetter
    @Override
    public Object get(Object key) {
        return getMember(key.toString());
    }

    @Override
    public Object remove(Object key) {
        if (readOnlyProperties.contains(key)) {
            throw new IllegalArgumentException("Property '" + key + "' is read-only");
        }
        return properties.remove(key);
    }

    @Override
    public int size() {
        return keySet().size();
    }

    @Override
    public boolean isEmpty() {
        return properties.isEmpty() && propertySuppliers.isEmpty();
    }

    @Override
    public boolean containsValue(Object value) {
        return properties.containsValue(value);
    }

    @Override
    public void clear() {
        properties.clear();
    }

    @NotNull
    @Override
    public Set<Entry<String, Object>> entrySet() {
        MutableSet<Entry<String, Object>> entries = UnifiedSet.newSet();
        entries.addAll(properties.entrySet());
        propertySuppliers.forEachKeyValue((key, supplier) -> {
            if (!properties.containsKey(key)) {
                entries.add(new AbstractMap.SimpleImmutableEntry<>(key, supplier.get()));
            }
        });
        return entries.asUnmodifiable();
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object getMember(String name) {
        Object obj = properties.computeIfAbsent(name,
                s -> propertySuppliers.containsKey(s) ? propertySuppliers.get(s).get() : null);
        if (Map.class.isInstance(obj)) {
            return new JavaScriptObject((Map<String, Object>) obj, null, null, parent);
        } else if (List.class.isInstance(obj)) {
            return new JavaScriptArray((List) obj);
        } else {
            return obj;
        }
    }

    @Override
    public boolean hasMember(String name) {
        return hasProperty(name);
    }

    @Override
    public void removeMember(String name) {
        remove(name);
    }

    @Override
    public void setMember(String name, Object value) {
        put(name, value);
    }

    @Override
    public Set<String> keySet() {
        MutableSet<String> keys = UnifiedSet.newSet();
        properties.forEachKey(keys::add);
        propertySuppliers.forEachKey(keys::add);
        return keys.asUnmodifiable();
    }

    @Override
    public Collection<Object> values() {
        return ((MutableSet<Entry<String, Object>>) entrySet()).collect(Entry<String, Object>::getValue);
    }

    @Override
    public boolean isInstance(Object instance) {
        return JavaScriptObject.class.isInstance(instance);
    }

    @Override
    public boolean isFunction() {
        return hasMember("apply");
    }

    @Override
    public boolean isStrictFunction() {
        return false;
    }

    @Override
    public String getClassName() {
        return "JavaScriptObject";
    }

    private boolean hasProperty(Object prop) {
        return null != getProperty(prop);
    }

    private Object getProperty(Object prop) {
        String key = prop.toString();

        if (properties.containsKey(key) || propertySuppliers.containsKey(key)) {
            return properties.getIfAbsentPut(key,
                    () -> propertySuppliers.getIfAbsentValue(key, NULL_SUPPLIER).get());
        } else if (null != parent) {
            return parentType.get(parent, key);
        } else {
            return null;
        }
    }

    @Override
    public void recycle() {
        clear();
    }

    public JavaScriptObject copyOf() {
        try {
            return (JavaScriptObject) clone();
        } catch (CloneNotSupportedException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return new JavaScriptObject(properties, propertySuppliers, readOnlyProperties.castToList(), parent);
    }

    @Override
    public String toString() {
        return "JavaScriptObject{" + "properties=" + properties + ", propertySuppliers=" + propertySuppliers
                + ", readOnlyProperties=" + readOnlyProperties + ", parent=" + parent + '}';
    }

    @Override
    public boolean equals(Object obj) {
        if (!JavaScriptObject.class.isInstance(obj)) {
            return false;
        }
        JavaScriptObject other = (JavaScriptObject) obj;

        return other.properties.equals(properties) && other.propertySuppliers.equals(propertySuppliers)
                && other.readOnlyProperties.equals(readOnlyProperties) && other.parent == parent;
    }

    @Override
    public int hashCode() {
        int result = properties != null ? properties.hashCode() : 0;
        result = 31 * result + (propertySuppliers != null ? propertySuppliers.hashCode() : 0);
        result = 31 * result + (readOnlyProperties != null ? readOnlyProperties.hashCode() : 0);
        result = 31 * result + (parent != null ? parent.hashCode() : 0);
        return result;
    }

    private static class FieldAccessor implements Function<Object, Object> {
        private Field field;

        private FieldAccessor(Object obj, String field) {
            try {
                this.field = obj.getClass().getDeclaredField(field);
                this.field.setAccessible(true);
            } catch (NoSuchFieldException e) {
            }
        }

        @Override
        public Object apply(Object o) {
            try {
                return (null != field ? field.get(o) : null);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
    }

}