1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.nashorn.api.scripting;
  26 
  27 import javax.script.Bindings;
  28 import javax.script.ScriptContext;
  29 import javax.script.ScriptEngine;
  30 import javax.script.ScriptEngineManager;
  31 import javax.script.ScriptException;
  32 import javax.script.SimpleBindings;
  33 import javax.script.SimpleScriptContext;
  34 import org.testng.Assert;
  35 import static org.testng.Assert.assertEquals;
  36 import static org.testng.Assert.assertNotNull;
  37 import static org.testng.Assert.assertTrue;
  38 import static org.testng.Assert.fail;
  39 import org.testng.annotations.Test;
  40 
  41 /**
  42  * Tests for jsr223 Bindings "scope" (engine, global scopes)
  43  */
  44 public class ScopeTest {
  45 
  46     @Test
  47     public void createBindingsTest() {
  48         final ScriptEngineManager m = new ScriptEngineManager();
  49         final ScriptEngine e = m.getEngineByName("nashorn");
  50         Bindings b = e.createBindings();
  51         b.put("foo", 42.0);
  52         Object res = null;
  53         try {
  54             res = e.eval("foo == 42.0", b);
  55         } catch (final ScriptException | NullPointerException se) {
  56             se.printStackTrace();
  57             fail(se.getMessage());
  58         }
  59 
  60         assertEquals(res, Boolean.TRUE);
  61     }
  62 
  63     @Test
  64     public void engineScopeTest() {
  65         final ScriptEngineManager m = new ScriptEngineManager();
  66         final ScriptEngine e = m.getEngineByName("nashorn");
  67         Bindings engineScope = e.getBindings(ScriptContext.ENGINE_SCOPE);
  68 
  69         // check few ECMA standard built-in global properties
  70         assertNotNull(engineScope.get("Object"));
  71         assertNotNull(engineScope.get("TypeError"));
  72         assertNotNull(engineScope.get("eval"));
  73 
  74         // can access via ScriptEngine.get as well
  75         assertNotNull(e.get("Object"));
  76         assertNotNull(e.get("TypeError"));
  77         assertNotNull(e.get("eval"));
  78 
  79         // Access by either way should return same object
  80         assertEquals(engineScope.get("Array"), e.get("Array"));
  81         assertEquals(engineScope.get("EvalError"), e.get("EvalError"));
  82         assertEquals(engineScope.get("undefined"), e.get("undefined"));
  83 
  84         // try exposing a new variable from scope
  85         engineScope.put("myVar", "foo");
  86         try {
  87             assertEquals(e.eval("myVar"), "foo");
  88         } catch (final ScriptException se) {
  89             se.printStackTrace();
  90             fail(se.getMessage());
  91         }
  92 
  93         // update "myVar" in script an check the value from scope
  94         try {
  95             e.eval("myVar = 'nashorn';");
  96         } catch (final ScriptException se) {
  97             se.printStackTrace();
  98             fail(se.getMessage());
  99         }
 100 
 101         // now check modified value from scope and engine
 102         assertEquals(engineScope.get("myVar"), "nashorn");
 103         assertEquals(e.get("myVar"), "nashorn");
 104     }
 105 
 106     @Test
 107     public void multiGlobalTest() {
 108         final ScriptEngineManager m = new ScriptEngineManager();
 109         final ScriptEngine e = m.getEngineByName("nashorn");
 110         final Bindings b = e.createBindings();
 111         final ScriptContext newCtxt = new SimpleScriptContext();
 112         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 113 
 114         try {
 115             Object obj1 = e.eval("Object");
 116             Object obj2 = e.eval("Object", newCtxt);
 117             Assert.assertNotEquals(obj1, obj2);
 118             Assert.assertNotNull(obj1);
 119             Assert.assertNotNull(obj2);
 120             Assert.assertEquals(obj1.toString(), obj2.toString());
 121 
 122             e.eval("x = 'hello'");
 123             e.eval("x = 'world'", newCtxt);
 124             Object x1 = e.getContext().getAttribute("x");
 125             Object x2 = newCtxt.getAttribute("x");
 126             Assert.assertNotEquals(x1, x2);
 127             Assert.assertEquals(x1, "hello");
 128             Assert.assertEquals(x2, "world");
 129 
 130             x1 = e.eval("x");
 131             x2 = e.eval("x", newCtxt);
 132             Assert.assertNotEquals(x1, x2);
 133             Assert.assertEquals(x1, "hello");
 134             Assert.assertEquals(x2, "world");
 135 
 136             final ScriptContext origCtxt = e.getContext();
 137             e.setContext(newCtxt);
 138             e.eval("y = new Object()");
 139             e.eval("y = new Object()", origCtxt);
 140 
 141             Object y1 = origCtxt.getAttribute("y");
 142             Object y2 = newCtxt.getAttribute("y");
 143             Assert.assertNotEquals(y1, y2);
 144             Assert.assertNotEquals(e.eval("y"), e.eval("y", origCtxt));
 145             Assert.assertEquals("[object Object]", y1.toString());
 146             Assert.assertEquals("[object Object]", y2.toString());
 147         } catch (final ScriptException se) {
 148             se.printStackTrace();
 149             fail(se.getMessage());
 150         }
 151     }
 152 
 153     @Test
 154     public void userEngineScopeBindingsTest() throws ScriptException {
 155         final ScriptEngineManager m = new ScriptEngineManager();
 156         final ScriptEngine e = m.getEngineByName("nashorn");
 157         e.eval("function func() {}");
 158 
 159         final ScriptContext newContext = new SimpleScriptContext();
 160         newContext.setBindings(new SimpleBindings(), ScriptContext.ENGINE_SCOPE);
 161         // we are using a new bindings - so it should have 'func' defined
 162         Object value = e.eval("typeof func", newContext);
 163         assertTrue(value.equals("undefined"));
 164     }
 165 
 166     @Test
 167     public void userEngineScopeBindingsNoLeakTest() throws ScriptException {
 168         final ScriptEngineManager m = new ScriptEngineManager();
 169         final ScriptEngine e = m.getEngineByName("nashorn");
 170         final ScriptContext newContext = new SimpleScriptContext();
 171         newContext.setBindings(new SimpleBindings(), ScriptContext.ENGINE_SCOPE);
 172         e.eval("function foo() {}", newContext);
 173 
 174         // in the default context's ENGINE_SCOPE, 'foo' shouldn't exist
 175         assertTrue(e.eval("typeof foo").equals("undefined"));
 176     }
 177 
 178     @Test
 179     public void userEngineScopeBindingsRetentionTest() throws ScriptException {
 180         final ScriptEngineManager m = new ScriptEngineManager();
 181         final ScriptEngine e = m.getEngineByName("nashorn");
 182         final ScriptContext newContext = new SimpleScriptContext();
 183         newContext.setBindings(new SimpleBindings(), ScriptContext.ENGINE_SCOPE);
 184         e.eval("function foo() {}", newContext);
 185 
 186         // definition retained with user's ENGINE_SCOPE Binding
 187         assertTrue(e.eval("typeof foo", newContext).equals("function"));
 188 
 189         final Bindings oldBindings = newContext.getBindings(ScriptContext.ENGINE_SCOPE);
 190         // but not in another ENGINE_SCOPE binding
 191         newContext.setBindings(new SimpleBindings(), ScriptContext.ENGINE_SCOPE);
 192         assertTrue(e.eval("typeof foo", newContext).equals("undefined"));
 193 
 194         // restore ENGINE_SCOPE and check again
 195         newContext.setBindings(oldBindings, ScriptContext.ENGINE_SCOPE);
 196         assertTrue(e.eval("typeof foo", newContext).equals("function"));
 197     }
 198 
 199     @Test
 200     // check that engine.js definitions are visible in all new global instances
 201     public void checkBuiltinsInNewBindingsTest() throws ScriptException {
 202         final ScriptEngineManager m = new ScriptEngineManager();
 203         final ScriptEngine e = m.getEngineByName("nashorn");
 204 
 205         // check default global instance has engine.js definitions
 206         final Bindings g = (Bindings) e.eval("this");
 207         Object value = g.get("__noSuchProperty__");
 208         assertTrue(value instanceof ScriptObjectMirror && ((ScriptObjectMirror)value).isFunction());
 209         value = g.get("print");
 210         assertTrue(value instanceof ScriptObjectMirror && ((ScriptObjectMirror)value).isFunction());
 211 
 212         // check new global instance created has engine.js definitions
 213         Bindings b = e.createBindings();
 214         value = b.get("__noSuchProperty__");
 215         assertTrue(value instanceof ScriptObjectMirror && ((ScriptObjectMirror)value).isFunction());
 216         value = b.get("print");
 217         assertTrue(value instanceof ScriptObjectMirror && ((ScriptObjectMirror)value).isFunction());
 218 
 219         // put a mapping into GLOBAL_SCOPE
 220         final Bindings globalScope = e.getContext().getBindings(ScriptContext.GLOBAL_SCOPE);
 221         globalScope.put("x", "hello");
 222 
 223         // GLOBAL_SCOPE mapping should be visible from default ScriptContext eval
 224         assertTrue(e.eval("x").equals("hello"));
 225 
 226         final ScriptContext ctx = new SimpleScriptContext();
 227         ctx.setBindings(globalScope, ScriptContext.GLOBAL_SCOPE);
 228         ctx.setBindings(b, ScriptContext.ENGINE_SCOPE);
 229 
 230         // GLOBAL_SCOPE mapping should be visible from non-default ScriptContext eval
 231         assertTrue(e.eval("x", ctx).equals("hello"));
 232 
 233         // try some arbitray Bindings for ENGINE_SCOPE
 234         Bindings sb = new SimpleBindings();
 235         ctx.setBindings(sb, ScriptContext.ENGINE_SCOPE);
 236 
 237         // GLOBAL_SCOPE mapping should be visible from non-default ScriptContext eval
 238         assertTrue(e.eval("x", ctx).equals("hello"));
 239 
 240         // engine.js builtins are still defined even with arbitrary Bindings
 241         assertTrue(e.eval("typeof print", ctx).equals("function"));
 242         assertTrue(e.eval("typeof __noSuchProperty__", ctx).equals("function"));
 243 
 244         // ENGINE_SCOPE definition should 'hide' GLOBAL_SCOPE definition
 245         sb.put("x", "newX");
 246         assertTrue(e.eval("x", ctx).equals("newX"));
 247     }
 248 
 249     @Test
 250     public static void multiThreadedVarTest() throws ScriptException, InterruptedException {
 251         final ScriptEngineManager m = new ScriptEngineManager();
 252         final ScriptEngine e = m.getEngineByName("nashorn");
 253         final Bindings b = e.createBindings();
 254         final ScriptContext origContext = e.getContext();
 255         final ScriptContext newCtxt = new SimpleScriptContext();
 256         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 257         final String sharedScript = "foo";
 258 
 259         assertEquals(e.eval("var foo = 'original context';", origContext), null);
 260         assertEquals(e.eval("var foo = 'new context';", newCtxt), null);
 261 
 262         final Thread t1 = new Thread(new ScriptRunner(e, origContext, sharedScript, "original context", 1000));
 263         final Thread t2 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, "new context", 1000));
 264         t1.start();
 265         t2.start();
 266         t1.join();
 267         t2.join();
 268 
 269         assertEquals(e.eval("var foo = 'newer context';", newCtxt), null);
 270         final Thread t3 = new Thread(new ScriptRunner(e, origContext, sharedScript, "original context", 1000));
 271         final Thread t4 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, "newer context", 1000));
 272 
 273         t3.start();
 274         t4.start();
 275         t3.join();
 276         t4.join();
 277 
 278         assertEquals(e.eval(sharedScript), "original context");
 279         assertEquals(e.eval(sharedScript, newCtxt), "newer context");
 280     }
 281 
 282     @Test
 283     public static void multiThreadedGlobalTest() throws ScriptException, InterruptedException {
 284         final ScriptEngineManager m = new ScriptEngineManager();
 285         final ScriptEngine e = m.getEngineByName("nashorn");
 286         final Bindings b = e.createBindings();
 287         final ScriptContext origContext = e.getContext();
 288         final ScriptContext newCtxt = new SimpleScriptContext();
 289         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 290 
 291         assertEquals(e.eval("foo = 'original context';", origContext), "original context");
 292         assertEquals(e.eval("foo = 'new context';", newCtxt), "new context");
 293         final String sharedScript = "foo";
 294 
 295         final Thread t1 = new Thread(new ScriptRunner(e, origContext, sharedScript, "original context", 1000));
 296         final Thread t2 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, "new context", 1000));
 297         t1.start();
 298         t2.start();
 299         t1.join();
 300         t2.join();
 301 
 302         Object obj3 = e.eval("delete foo; foo = 'newer context';", newCtxt);
 303         assertEquals(obj3, "newer context");
 304         final Thread t3 = new Thread(new ScriptRunner(e, origContext, sharedScript, "original context", 1000));
 305         final Thread t4 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, "newer context", 1000));
 306 
 307         t3.start();
 308         t4.start();
 309         t3.join();
 310         t4.join();
 311 
 312         Assert.assertEquals(e.eval(sharedScript), "original context");
 313         Assert.assertEquals(e.eval(sharedScript, newCtxt), "newer context");
 314     }
 315 
 316     @Test
 317     public static void multiThreadedIncTest() throws ScriptException, InterruptedException {
 318         final ScriptEngineManager m = new ScriptEngineManager();
 319         final ScriptEngine e = m.getEngineByName("nashorn");
 320         final Bindings b = e.createBindings();
 321         final ScriptContext origContext = e.getContext();
 322         final ScriptContext newCtxt = new SimpleScriptContext();
 323         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 324 
 325         assertEquals(e.eval("var x = 0;", origContext), null);
 326         assertEquals(e.eval("var x = 2;", newCtxt), null);
 327         final String sharedScript = "x++;";
 328 
 329         final Thread t1 = new Thread(new Runnable() {
 330             @Override
 331             public void run() {
 332                 try {
 333                     for (int i = 0; i < 1000; i++) {
 334                         assertEquals(e.eval(sharedScript, origContext), (double)i);
 335                     }
 336                 } catch (ScriptException se) {
 337                     fail(se.toString());
 338                 }
 339             }
 340         });
 341         final Thread t2 = new Thread(new Runnable() {
 342             @Override
 343             public void run() {
 344                 try {
 345                     for (int i = 2; i < 1000; i++) {
 346                         assertEquals(e.eval(sharedScript, newCtxt), (double)i);
 347                     }
 348                 } catch (ScriptException se) {
 349                     fail(se.toString());
 350                 }
 351             }
 352         });
 353         t1.start();
 354         t2.start();
 355         t1.join();
 356         t2.join();
 357     }
 358 
 359     @Test
 360     public static void multiThreadedPrimitiveTest() throws ScriptException, InterruptedException {
 361         final ScriptEngineManager m = new ScriptEngineManager();
 362         final ScriptEngine e = m.getEngineByName("nashorn");
 363         final Bindings b = e.createBindings();
 364         final ScriptContext origContext = e.getContext();
 365         final ScriptContext newCtxt = new SimpleScriptContext();
 366         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 367 
 368         Object obj1 = e.eval("String.prototype.foo = 'original context';", origContext);
 369         Object obj2 = e.eval("String.prototype.foo = 'new context';", newCtxt);
 370         assertEquals(obj1, "original context");
 371         assertEquals(obj2, "new context");
 372         final String sharedScript = "''.foo";
 373 
 374         final Thread t1 = new Thread(new ScriptRunner(e, origContext, sharedScript, "original context", 1000));
 375         final Thread t2 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, "new context", 1000));
 376         t1.start();
 377         t2.start();
 378         t1.join();
 379         t2.join();
 380 
 381         Object obj3 = e.eval("delete String.prototype.foo; Object.prototype.foo = 'newer context';", newCtxt);
 382         assertEquals(obj3, "newer context");
 383         final Thread t3 = new Thread(new ScriptRunner(e, origContext, sharedScript, "original context", 1000));
 384         final Thread t4 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, "newer context", 1000));
 385 
 386         t3.start();
 387         t4.start();
 388         t3.join();
 389         t4.join();
 390 
 391         Assert.assertEquals(e.eval(sharedScript), "original context");
 392         Assert.assertEquals(e.eval(sharedScript, newCtxt), "newer context");
 393     }
 394 
 395     @Test
 396     public static void multiThreadedFunctionTest() throws ScriptException, InterruptedException {
 397         final ScriptEngineManager m = new ScriptEngineManager();
 398         final ScriptEngine e = m.getEngineByName("nashorn");
 399         final Bindings b = e.createBindings();
 400         final ScriptContext origContext = e.getContext();
 401         final ScriptContext newCtxt = new SimpleScriptContext();
 402         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 403 
 404         e.eval(new URLReader(ScopeTest.class.getResource("resources/func.js")), origContext);
 405         assertEquals(origContext.getAttribute("scopeVar"), 1);
 406         assertEquals(e.eval("scopeTest()"), 1);
 407 
 408         e.eval(new URLReader(ScopeTest.class.getResource("resources/func.js")), newCtxt);
 409         assertEquals(newCtxt.getAttribute("scopeVar"), 1);
 410         assertEquals(e.eval("scopeTest();", newCtxt), 1);
 411 
 412         assertEquals(e.eval("scopeVar = 3;", newCtxt), 3);
 413         assertEquals(newCtxt.getAttribute("scopeVar"), 3);
 414 
 415 
 416         final Thread t1 = new Thread(new ScriptRunner(e, origContext, "scopeTest()", 1, 1000));
 417         final Thread t2 = new Thread(new ScriptRunner(e, newCtxt, "scopeTest()", 3, 1000));
 418 
 419         t1.start();
 420         t2.start();
 421         t1.join();
 422         t2.join();
 423 
 424     }
 425 
 426     @Test
 427     public static void getterSetterTest() throws ScriptException, InterruptedException {
 428         final ScriptEngineManager m = new ScriptEngineManager();
 429         final ScriptEngine e = m.getEngineByName("nashorn");
 430         final Bindings b = e.createBindings();
 431         final ScriptContext origContext = e.getContext();
 432         final ScriptContext newCtxt = new SimpleScriptContext();
 433         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 434         final String sharedScript = "accessor1";
 435 
 436         e.eval(new URLReader(ScopeTest.class.getResource("resources/gettersetter.js")), origContext);
 437         assertEquals(e.eval("accessor1 = 1;"), 1);
 438         assertEquals(e.eval(sharedScript), 1);
 439 
 440         e.eval(new URLReader(ScopeTest.class.getResource("resources/gettersetter.js")), newCtxt);
 441         assertEquals(e.eval("accessor1 = 2;", newCtxt), 2);
 442         assertEquals(e.eval(sharedScript, newCtxt), 2);
 443 
 444 
 445         final Thread t1 = new Thread(new ScriptRunner(e, origContext, sharedScript, 1, 1000));
 446         final Thread t2 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, 2, 1000));
 447 
 448         t1.start();
 449         t2.start();
 450         t1.join();
 451         t2.join();
 452 
 453         assertEquals(e.eval(sharedScript), 1);
 454         assertEquals(e.eval(sharedScript, newCtxt), 2);
 455         assertEquals(e.eval("v"), 1);
 456         assertEquals(e.eval("v", newCtxt), 2);
 457     }
 458 
 459     @Test
 460     public static void getterSetter2Test() throws ScriptException, InterruptedException {
 461         final ScriptEngineManager m = new ScriptEngineManager();
 462         final ScriptEngine e = m.getEngineByName("nashorn");
 463         final Bindings b = e.createBindings();
 464         final ScriptContext origContext = e.getContext();
 465         final ScriptContext newCtxt = new SimpleScriptContext();
 466         newCtxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 467         final String sharedScript = "accessor2";
 468 
 469         e.eval(new URLReader(ScopeTest.class.getResource("resources/gettersetter.js")), origContext);
 470         assertEquals(e.eval("accessor2 = 1;"), 1);
 471         assertEquals(e.eval(sharedScript), 1);
 472 
 473         e.eval(new URLReader(ScopeTest.class.getResource("resources/gettersetter.js")), newCtxt);
 474         assertEquals(e.eval("accessor2 = 2;", newCtxt), 2);
 475         assertEquals(e.eval(sharedScript, newCtxt), 2);
 476 
 477 
 478         final Thread t1 = new Thread(new ScriptRunner(e, origContext, sharedScript, 1, 1000));
 479         final Thread t2 = new Thread(new ScriptRunner(e, newCtxt, sharedScript, 2, 1000));
 480 
 481         t1.start();
 482         t2.start();
 483         t1.join();
 484         t2.join();
 485 
 486         assertEquals(e.eval(sharedScript), 1);
 487         assertEquals(e.eval(sharedScript, newCtxt), 2);
 488         assertEquals(e.eval("x"), 1);
 489         assertEquals(e.eval("x", newCtxt), 2);
 490     }
 491 
 492     @Test
 493     public static void testSlowScope() throws ScriptException, InterruptedException {
 494         final ScriptEngineManager m = new ScriptEngineManager();
 495         final ScriptEngine e = m.getEngineByName("nashorn");
 496 
 497         for (int i = 0; i < 100; i++) {
 498             final Bindings b = e.createBindings();
 499             final ScriptContext ctxt = new SimpleScriptContext();
 500             ctxt.setBindings(b, ScriptContext.ENGINE_SCOPE);
 501 
 502             e.eval(new URLReader(ScopeTest.class.getResource("resources/witheval.js")), ctxt);
 503             assertEquals(e.eval("a", ctxt), 1);
 504             assertEquals(b.get("a"), 1);
 505             assertEquals(e.eval("b", ctxt), 3);
 506             assertEquals(b.get("b"), 3);
 507             assertEquals(e.eval("c", ctxt), 10);
 508             assertEquals(b.get("c"), 10);
 509         }
 510     }
 511 
 512     private static class ScriptRunner implements Runnable {
 513 
 514         final ScriptEngine engine;
 515         final ScriptContext context;
 516         final String source;
 517         final Object expected;
 518         final int iterations;
 519 
 520         ScriptRunner(final ScriptEngine engine, final ScriptContext context, final String source, final Object expected, final int iterations) {
 521             this.engine = engine;
 522             this.context = context;
 523             this.source = source;
 524             this.expected = expected;
 525             this.iterations = iterations;
 526         }
 527 
 528         @Override
 529         public void run() {
 530             try {
 531                 for (int i = 0; i < iterations; i++) {
 532                     assertEquals(engine.eval(source, context), expected);
 533                 }
 534             } catch (ScriptException se) {
 535                 throw new RuntimeException(se);
 536             }
 537         }
 538     }
 539 
 540 }
--- EOF ---