View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.scxml2.env.javascript;
19  
20  import java.util.Collection;
21  import java.util.HashSet;
22  import java.util.Map;
23  import java.util.Set;
24  
25  import javax.script.Bindings;
26  import javax.script.SimpleBindings;
27  
28  import org.apache.commons.scxml2.Context;
29  
30  /**
31   * Wrapper class for the JDK Javascript engine Bindings class that extends the
32   * wrapped Bindings to search the SCXML context for variables and predefined
33   * functions that do not exist in the wrapped Bindings.
34   *
35   */
36  public class JSBindings implements Bindings {
37  
38      private static final String NASHORN_GLOBAL = "nashorn.global";
39  
40      // INSTANCE VARIABLES
41  
42      private Bindings bindings;
43      private Context  context;
44  
45      // CONSTRUCTORS
46  
47      /**
48       * Initialises the internal Bindings delegate and SCXML context.
49       *
50       * @param context  SCXML Context to use for script variables.
51       * @param bindings Javascript engine bindings for Javascript variables.
52       *
53       * @throws IllegalArgumentException Thrown if either <code>context</code>
54       *         or <code>bindings</code> is <code>null</code>.
55       *
56       */
57      public JSBindings(Context context, Bindings bindings) {
58          // ... validate
59  
60          if (context == null) {
61             throw new IllegalArgumentException("Invalid SCXML context");
62          }
63  
64          if (bindings == null) {
65             throw new IllegalArgumentException("Invalid script Bindings");
66          }
67  
68          // ... initialise
69  
70          this.bindings = bindings;
71          this.context = context;
72      }
73  
74      // INSTANCE METHODS
75  
76      /**
77       * Returns <code>true</code> if the wrapped Bindings delegate
78       * or SCXML context  contains a variable identified by
79       * <code>key</code>.
80       *
81       */
82      @Override
83      public boolean containsKey(Object key) {
84          if (hasGlobalBindings() && getGlobalBindings().containsKey(key)) {
85              return true;
86          }
87  
88          if (bindings.containsKey(key)) {
89              return true;
90          }
91  
92          return context.has(key.toString());
93      }
94  
95      /**
96       * Returns a union of the wrapped Bindings entry set and the
97       * SCXML context entry set.
98       * <p>
99       * 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 }