summaryrefslogtreecommitdiff
path: root/plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptValueConverter.java
blob: a90948c187768a0d2fe1997ce4d2246cde7dc822 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.script.javascript.support;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.IdScriptableObject;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Wrapper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Value Converter to marshal objects between Java and Javascript.
 *
 *
 */
public final class ScriptValueConverter {
    private static final String TYPE_DATE = "Date";


    /**
     * Private constructor - methods are static
     */
    private ScriptValueConverter() {
    }

    /**
     * Convert an object from a script wrapper value to a serializable value valid outside
     * of the Rhino script processor context.
     * <p>
     * This includes converting JavaScript Array objects to Lists of valid objects.
     *
     * @param value Value to convert from script wrapper object to external object value.
     * @return unwrapped and converted value.
     */
    public static Object unwrapValue(Object value) {
        if (value == null) {
            return null;
        } else if (value instanceof Wrapper) {
            // unwrap a Java object from a JavaScript wrapper
            // recursively call this method to convert the unwrapped value
            value = unwrapValue(((Wrapper) value).unwrap());
        } else if (value instanceof IdScriptableObject) {
            // check for special case Native object wrappers
            String className = ((IdScriptableObject) value).getClassName();
            // check for special case of the String object
            if ("String".equals(className)) {
                value = Context.jsToJava(value, String.class);
            }
            // check for special case of a Date object
            else if ("Date".equals(className)) {
                value = Context.jsToJava(value, Date.class);
            } else {
                // a scriptable object will probably indicate a multi-value property set
                // set using a JavaScript associative Array object
                Scriptable values = (Scriptable) value;
                Object[] propIds = values.getIds();

                // is it a JavaScript associative Array object using Integer indexes?
                if (values instanceof NativeArray && isArray(propIds)) {
                    // convert JavaScript array of values to a List of Serializable objects
                    List<Object> propValues = new ArrayList<Object>(propIds.length);
                    for (int i = 0; i < propIds.length; i++) {
                        // work on each key in turn
                        Integer propId = (Integer) propIds[i];

                        // we are only interested in keys that indicate a list of values
                        if (propId instanceof Integer) {
                            // get the value out for the specified key
                            Object val = values.get(propId, values);
                            // recursively call this method to convert the value
                            propValues.add(unwrapValue(val));
                        }
                    }

                    value = propValues;
                } else {
                    // any other JavaScript object that supports properties - convert to a Map of objects
                    Map<String, Object> propValues = new HashMap<String, Object>(propIds.length);
                    for (int i = 0; i < propIds.length; i++) {
                        // work on each key in turn
                        Object propId = propIds[i];

                        // we are only interested in keys that indicate a list of values
                        if (propId instanceof String) {
                            // get the value out for the specified key
                            Object val = values.get((String) propId, values);
                            // recursively call this method to convert the value
                            propValues.put((String) propId, unwrapValue(val));
                        }
                    }
                    value = propValues;
                }
            }
        } else if (value instanceof Object[]) {
            // convert back a list Object Java values
            Object[] array = (Object[]) value;
            ArrayList<Object> list = new ArrayList<Object>(array.length);
            for (int i = 0; i < array.length; i++) {
                list.add(unwrapValue(array[i]));
            }
            value = list;
        } else if (value instanceof Map) {
            // ensure each value in the Map is unwrapped (which may have been an unwrapped NativeMap!)
            Map<Object, Object> map = (Map<Object, Object>) value;
            Map<Object, Object> copyMap = new HashMap<Object, Object>(map.size());
            for (Object key : map.keySet()) {
                copyMap.put(key, unwrapValue(map.get(key)));
            }
            value = copyMap;
        }
        return value;
    }

    /**
     * Convert an object from any repository serialized value to a valid script object.
     * This includes converting Collection multi-value properties into JavaScript Array objects.
     *
     * @param scope Scripting scope
     * @param value Property value
     * @return Value safe for scripting usage
     */
    public static Object wrapValue(Scriptable scope, Object value) {
        // perform conversions from Java objects to JavaScript scriptable instances
        if (value == null) {
            return null;
        } else if (value instanceof Date) {
            // convert Date to JavaScript native Date object
            // call the "Date" constructor on the root scope object - passing in the millisecond
            // value from the Java date - this will construct a JavaScript Date with the same value
            Date date = (Date) value;
            value = ScriptRuntime.newObject(
                    Context.getCurrentContext(), scope, TYPE_DATE, new Object[]{date.getTime()});
        } else if (value instanceof Collection) {
            // recursively convert each value in the collection
            Collection<Object> collection = (Collection<Object>) value;
            Object[] array = new Object[collection.size()];
            int index = 0;
            for (Object obj : collection) {
                array[index++] = wrapValue(scope, obj);
            }
            // convert array to a native JavaScript Array
            value = Context.getCurrentContext().newArray(scope, array);
        } else if (value instanceof Map) {
            value = NativeMap.wrap(scope, (Map) value);
        }

        // simple numbers, strings and booleans are wrapped automatically by Rhino

        return value;
    }

    /**
     * Look at the id's of a native array and try to determine whether it's actually an Array or a Hashmap
     *
     * @param ids id's of the native array
     * @return boolean  true if it's an array, false otherwise (ie it's a map)
     */
    private static boolean isArray(final Object[] ids) {
        boolean result = true;
        for (int i = 0; i < ids.length; i++) {
            if (ids[i] instanceof Integer == false) {
                result = false;
                break;
            }
        }
        return result;
    }
}