001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.scxml2.env.javascript;
019
020import java.util.Collection;
021import java.util.HashSet;
022import java.util.Map;
023import java.util.Set;
024
025import javax.script.Bindings;
026import javax.script.SimpleBindings;
027
028import org.apache.commons.scxml2.Context;
029
030/**
031 * Wrapper class for the JDK Javascript engine Bindings class that extends the
032 * wrapped Bindings to search the SCXML context for variables and predefined
033 * functions that do not exist in the wrapped Bindings.
034 *
035 */
036public class JSBindings implements Bindings {
037
038    private static final String NASHORN_GLOBAL = "nashorn.global";
039
040    // INSTANCE VARIABLES
041
042    private Bindings bindings;
043    private Context  context;
044
045    // CONSTRUCTORS
046
047    /**
048     * Initialises the internal Bindings delegate and SCXML context.
049     *
050     * @param context  SCXML Context to use for script variables.
051     * @param bindings Javascript engine bindings for Javascript variables.
052     *
053     * @throws IllegalArgumentException Thrown if either <code>context</code>
054     *         or <code>bindings</code> is <code>null</code>.
055     *
056     */
057    public JSBindings(Context context, Bindings bindings) {
058        // ... validate
059
060        if (context == null) {
061           throw new IllegalArgumentException("Invalid SCXML context");
062        }
063
064        if (bindings == null) {
065           throw new IllegalArgumentException("Invalid script Bindings");
066        }
067
068        // ... initialise
069
070        this.bindings = bindings;
071        this.context = context;
072    }
073
074    // INSTANCE METHODS
075
076    /**
077     * Returns <code>true</code> if the wrapped Bindings delegate
078     * or SCXML context  contains a variable identified by
079     * <code>key</code>.
080     *
081     */
082    @Override
083    public boolean containsKey(Object key) {
084        if (hasGlobalBindings() && getGlobalBindings().containsKey(key)) {
085            return true;
086        }
087
088        if (bindings.containsKey(key)) {
089            return true;
090        }
091
092        return context.has(key.toString());
093    }
094
095    /**
096     * Returns a union of the wrapped Bindings entry set and the
097     * SCXML context entry set.
098     * <p>
099     * NOTE: doesn't seem to be invoked ever. Not thread-safe.
100     *
101     */
102    @Override
103    public Set<String> keySet() {
104        Set<String> keys = new HashSet<String>();
105
106        keys.addAll(context.getVars().keySet());
107        keys.addAll(bindings.keySet());
108
109        if (hasGlobalBindings()) {
110            keys.addAll(getGlobalBindings().keySet());
111        }
112
113        return keys;
114    }
115
116    /**
117     * Returns the combined size of the wrapped Bindings entry set and the
118     * SCXML context entry set.
119     * <p>
120     * NOTE: doesn't seem to be invoked ever so not sure if it works in
121     *       context. Not thread-safe.
122     *
123     */
124    @Override
125    public int size() {
126        Set<String> keys = new HashSet<String>();
127
128        keys.addAll(context.getVars().keySet());
129        keys.addAll(bindings.keySet());
130
131        if (hasGlobalBindings()) {
132            keys.addAll(getGlobalBindings().keySet());
133        }
134
135        return keys.size();
136    }
137
138    /**
139     * Returns <code>true</code> if the wrapped Bindings delegate
140     * or SCXML context contains <code>value</code>.
141     * <p>
142     * NOTE: doesn't seem to be invoked ever so not sure if it works in
143     *       context. Not thread-safe.
144     */
145    @Override
146    public boolean containsValue(Object value) {
147        if (hasGlobalBindings() && getGlobalBindings().containsValue(value)) {
148            return true;
149        }
150
151        if (bindings.containsValue(value)) {
152            return true;
153        }
154
155        return context.getVars().containsValue(value);
156    }
157
158    /**
159     * Returns a union of the wrapped Bindings entry set and the
160     * SCXML context entry set.
161     * <p>
162     * NOTE: doesn't seem to be invoked ever so not sure if it works in
163     *       context. Not thread-safe.
164     */
165    @Override
166    public Set<Map.Entry<String,Object>> entrySet() {
167        return union().entrySet();
168    }
169
170    /**
171     * Returns a union of the wrapped Bindings value list and the
172     * SCXML context value list.
173     * <p>
174     * NOTE: doesn't seem to be invoked ever so not sure if it works in
175     *       context. Not thread-safe.
176     */
177    @Override
178    public Collection<Object> values() {
179        return union().values();
180    }
181
182    /**
183     * Returns a <code>true</code> if both the Bindings delegate and
184     * the SCXML context maps are empty.
185     * <p>
186     * NOTE: doesn't seem to be invoked ever so not sure if it works in
187     *       context. Not thread-safe.
188     */
189    @Override
190    public boolean isEmpty() {
191        if (hasGlobalBindings() && !getGlobalBindings().isEmpty()) {
192            return false;
193        }
194
195        if (!bindings.isEmpty()) {
196            return false;
197        }
198
199        return context.getVars().isEmpty();
200    }
201
202    /**
203     * Returns the value from the wrapped Bindings delegate
204     * or SCXML context contains identified by <code>key</code>.
205     *
206     */
207    @Override
208    public Object get(Object key) {
209        // nashorn.global should be retrieved from the bindings, not from context.
210        if (NASHORN_GLOBAL.equals(key)) {
211            return bindings.get(key);
212        }
213
214        if (hasGlobalBindings() && getGlobalBindings().containsKey(key)) {
215            return getGlobalBindings().get(key);
216        }
217
218        if (bindings.containsKey(key)) {
219            return bindings.get(key);
220        }
221
222        return context.get(key.toString());
223    }
224
225    /**
226     * The following delegation model is used to set values:
227     * <ol>
228     *   <li>Delegates to {@link Context#set(String,Object)} if the
229     *       {@link Context} contains the key (name), else</li>
230     *   <li>Delegates to the wrapped {@link Bindings#put(String, Object)}
231     *       if the {@link Bindings} contains the key (name), else</li>
232     *   <li>Delegates to {@link Context#setLocal(String, Object)}</li>
233     * </ol>
234     *
235     */
236    @Override
237    public Object put(String name, Object value) {
238        Object old = context.get(name);
239
240        // nashorn.global should be put into the bindings, not into context.
241        if (NASHORN_GLOBAL.equals(name)) {
242            return bindings.put(name, value);
243        } else if (context.has(name)) {
244            context.set(name, value);
245        } else if (bindings.containsKey(name)) {
246            return bindings.put(name, value);
247        } else if (hasGlobalBindings() && getGlobalBindings().containsKey(name)) {
248            return getGlobalBindings().put(name, value);
249        } else {
250            context.setLocal(name, value);
251        }
252
253        return old;
254    }
255
256    /**
257     * Delegates to the wrapped Bindings <code>putAll</code> method i.e. does
258     * not store variables in the SCXML context.
259     * <p>
260     * NOTE: doesn't seem to be invoked ever so not sure if it works in
261     *       context. Not thread-safe.
262     */
263    @Override
264    public void putAll(Map<? extends String, ? extends Object> toMerge) {
265        bindings.putAll(toMerge);
266    }
267
268    /**
269     * Removes the object from the wrapped Bindings instance or the contained
270     * SCXML context. Not entirely sure about this implementation but it
271     * follows the philosophy of using the Javascript Bindings as a child context
272     * of the SCXML context.
273     * <p>
274     * NOTE: doesn't seem to be invoked ever so not sure if it works in
275     *       context. Not thread-safe.
276     */
277    @Override
278    public Object remove(Object key) {
279        if (hasGlobalBindings() && getGlobalBindings().containsKey(key)) {
280            getGlobalBindings().remove(key);
281        }
282
283        if (bindings.containsKey(key)) {
284            return bindings.remove(key);
285        }
286
287        if (context.has(key.toString())) {
288            return context.getVars().remove(key);
289        }
290
291        return Boolean.FALSE;
292    }
293
294    /**
295     * Delegates to the wrapped Bindings <code>clear</code> method. Does not clear
296     * the SCXML context.
297     * <p>
298     * NOTE: doesn't seem to be invoked ever so not sure if it works in
299     *       context. Not thread-safe.
300     */
301    @Override
302    public void clear() {
303        bindings.clear();
304    }
305
306    /**
307     * Internal method to create a union of the SCXML context and the Javascript
308     * Bindings. Does a heavyweight copy - and so far only invoked by the
309     * not used methods.
310     */
311    private Bindings union() {
312        Bindings set = new SimpleBindings();
313
314        set.putAll(context.getVars());
315
316        for (String key : bindings.keySet()) {
317            set.put(key, bindings.get(key));
318        }
319
320        if (hasGlobalBindings()) {
321            for (String key : getGlobalBindings().keySet()) {
322                set.put(key, getGlobalBindings().get(key));
323            }
324        }
325
326        return set;
327    }
328
329    /**
330     * Return true if a global bindings (i.e. nashorn Global instance) was ever set by the script engine.
331     * <p>
332     * Note: because the global binding can be set by the script engine when evaluating a script, we should
333     *       check or retrieve the global binding whenever needed instead of initialization time.
334     * </p>
335     * @return true if a global bindings (i.e. nashorn Global instance) was ever set by the script engine
336     */
337    protected boolean hasGlobalBindings() {
338        if (bindings.containsKey(NASHORN_GLOBAL)) {
339            return true;
340        }
341
342        return false;
343    }
344
345    /**
346     * Return the global bindings (i.e. nashorn Global instance) set by the script engine if existing.
347     * @return the global bindings (i.e. nashorn Global instance) set by the script engine, or null if not existing.
348     */
349    protected Bindings getGlobalBindings() {
350        if (bindings.containsKey(NASHORN_GLOBAL)) {
351            return (Bindings) bindings.get(NASHORN_GLOBAL);
352        }
353
354        return null;
355    }
356}