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 }