/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF 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.apache.tinkerpop.gremlin.groovy.jsr223; import groovy.lang.MissingPropertyException; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.tinkerpop.gremlin.AbstractGremlinTest; import org.apache.tinkerpop.gremlin.FeatureRequirementSet; import org.apache.tinkerpop.gremlin.LoadGraphWith; import org.apache.tinkerpop.gremlin.groovy.CompilerCustomizerProvider; import org.apache.tinkerpop.gremlin.groovy.DefaultImportCustomizerProvider; import org.apache.tinkerpop.gremlin.groovy.NoImportCustomizerProvider; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.util.config.YamlConfiguration; import org.junit.Assert; import org.junit.Test; import javax.script.Bindings; import javax.script.CompiledScript; import javax.script.ScriptEngine; import javax.script.ScriptException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.concurrent.CountDownLatch; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * @author Stephen Mallette (http://stephen.genoprime.com) */ public class GremlinGroovyScriptEngineOverGraphTest extends AbstractGremlinTest { @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldDoSomeGremlin() throws Exception { final ScriptEngine engine = new GremlinGroovyScriptEngine(); final List list = new ArrayList(); final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("marko", convertToVertexId("marko")); bindings.put("temp", list); assertEquals(list.size(), 0); engine.eval("g.V(marko).out().fill(temp)",bindings); assertEquals(list.size(), 3); } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldLoadImports() throws Exception { final ScriptEngine engineNoImports = new GremlinGroovyScriptEngine((CompilerCustomizerProvider) NoImportCustomizerProvider.INSTANCE); try { engineNoImports.eval("Vertex.class.getName()"); fail("Should have thrown an exception because no imports were supplied"); } catch (Exception se) { assertTrue(se instanceof ScriptException); } final ScriptEngine engineWithImports = new GremlinGroovyScriptEngine((CompilerCustomizerProvider) new DefaultImportCustomizerProvider()); engineWithImports.put("g", g); assertEquals(Vertex.class.getName(), engineWithImports.eval("Vertex.class.getName()")); assertEquals(2l, engineWithImports.eval("g.V().has('age',gt(30)).count().next()")); assertEquals(Direction.IN, engineWithImports.eval("Direction.IN")); assertEquals(Direction.OUT, engineWithImports.eval("Direction.OUT")); assertEquals(Direction.BOTH, engineWithImports.eval("Direction.BOTH")); } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldLoadStandardImportsAndThenAddToThem() throws Exception { final GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine((CompilerCustomizerProvider) new DefaultImportCustomizerProvider()); engine.put("g", g); assertEquals(Vertex.class.getName(), engine.eval("Vertex.class.getName()")); assertEquals(2l, engine.eval("g.V().has('age',gt(30)).count().next()")); assertEquals(Direction.IN, engine.eval("Direction.IN")); assertEquals(Direction.OUT, engine.eval("Direction.OUT")); assertEquals(Direction.BOTH, engine.eval("Direction.BOTH")); try { engine.eval("YamlConfiguration.class.getName()"); fail("Should have thrown an exception because no imports were supplied"); } catch (Exception se) { assertTrue(se instanceof ScriptException); } engine.addImports(new HashSet<>(Arrays.asList("import " + YamlConfiguration.class.getCanonicalName()))); engine.put("g", g); assertEquals(YamlConfiguration.class.getName(), engine.eval("YamlConfiguration.class.getName()")); assertEquals(Vertex.class.getName(), engine.eval("Vertex.class.getName()")); assertEquals(2l, engine.eval("g.V().has('age',gt(30)).count().next()")); assertEquals(Direction.IN, engine.eval("Direction.IN")); assertEquals(Direction.OUT, engine.eval("Direction.OUT")); assertEquals(Direction.BOTH, engine.eval("Direction.BOTH")); } @Test @LoadGraphWith(LoadGraphWith.GraphData.CLASSIC) public void shouldProperlyHandleBindings() throws Exception { final ScriptEngine engine = new GremlinGroovyScriptEngine(); engine.put("g", g); engine.put("marko", convertToVertexId("marko")); Assert.assertEquals(g.V(convertToVertexId("marko")).next(), engine.eval("g.V(marko).next()")); final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("s", "marko"); bindings.put("f", 0.5f); bindings.put("i", 1); bindings.put("b", true); bindings.put("l", 100l); bindings.put("d", 1.55555d); assertEquals(engine.eval("g.E().has('weight',f).next()", bindings), g.E(convertToEdgeId("marko", "knows", "vadas")).next()); assertEquals(engine.eval("g.V().has('name',s).next()", bindings), g.V(convertToVertexId("marko")).next()); assertEquals(engine.eval("g.V().sideEffect{it.get().property('bbb',it.get().value('name')=='marko')}.iterate();g.V().has('bbb',b).next()", bindings), g.V(convertToVertexId("marko")).next()); assertEquals(engine.eval("g.V().sideEffect{it.get().property('iii',it.get().value('name')=='marko'?1:0)}.iterate();g.V().has('iii',i).next()", bindings), g.V(convertToVertexId("marko")).next()); assertEquals(engine.eval("g.V().sideEffect{it.get().property('lll',it.get().value('name')=='marko'?100l:0l)}.iterate();g.V().has('lll',l).next()", bindings), g.V(convertToVertexId("marko")).next()); assertEquals(engine.eval("g.V().sideEffect{it.get().property('ddd',it.get().value('name')=='marko'?1.55555d:0)}.iterate();g.V().has('ddd',d).next()", bindings), g.V(convertToVertexId("marko")).next()); } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldClearBindingsBetweenEvals() throws Exception { final ScriptEngine engine = new GremlinGroovyScriptEngine(); engine.put("g", g); engine.put("marko", convertToVertexId("marko")); assertEquals(g.V(convertToVertexId("marko")).next(), engine.eval("g.V(marko).next()")); final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("s", "marko"); assertEquals(engine.eval("g.V().has('name',s).next()", bindings), g.V(convertToVertexId("marko")).next()); try { engine.eval("g.V().has('name',s).next()"); fail("This should have failed because s is no longer bound"); } catch (Exception ex) { final Throwable t = ExceptionUtils.getRootCause(ex); assertEquals(MissingPropertyException.class, t.getClass()); assertTrue(t.getMessage().startsWith("No such property: s for class")); } } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldBeThreadSafe() throws Exception { final ScriptEngine engine = new GremlinGroovyScriptEngine(); int runs = 500; final CountDownLatch latch = new CountDownLatch(runs); final List<String> names = Arrays.asList("marko", "peter", "josh", "vadas", "stephen", "pavel", "matthias"); final Random random = new Random(); for (int i = 0; i < runs; i++) { new Thread("test-thread-safe-" + i) { public void run() { String name = names.get(random.nextInt(names.size() - 1)); try { final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("name", name); final Object result = engine.eval("t = g.V().has('name',name); if(t.hasNext()) { t } else { null }", bindings); if (name.equals("stephen") || name.equals("pavel") || name.equals("matthias")) assertNull(result); else assertNotNull(result); } catch (ScriptException e) { assertFalse(true); } finally { if (graph.features().graph().supportsTransactions()) g.tx().rollback(); } latch.countDown(); } }.start(); } latch.await(); } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldBeThreadSafeOnCompiledScript() throws Exception { final GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); final CompiledScript script = engine.compile("t = g.V().has('name',name); if(t.hasNext()) { t } else { null }"); int runs = 500; final CountDownLatch latch = new CountDownLatch(runs); final List<String> names = Arrays.asList("marko", "peter", "josh", "vadas", "stephen", "pavel", "matthias"); final Random random = new Random(); for (int i = 0; i < runs; i++) { new Thread("test-thread-safety-on-compiled-script-" + i) { public void run() { String name = names.get(random.nextInt(names.size() - 1)); try { final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("name", name); Object result = script.eval(bindings); if (name.equals("stephen") || name.equals("pavel") || name.equals("matthias")) assertNull(result); else assertNotNull(result); } catch (ScriptException e) { //System.out.println(e); assertFalse(true); } finally { if (graph.features().graph().supportsTransactions()) g.tx().rollback(); } latch.countDown(); } }.start(); } latch.await(); } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldEvalGlobalClosuresEvenAfterEvictionOfClass() throws ScriptException { final GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("marko", convertToVertexId("marko")); bindings.put("vadas", convertToVertexId("vadas")); // strong referenced global closure engine.eval("def isVadas(v){v.value('name')=='vadas'}", bindings); assertEquals(true, engine.eval("isVadas(g.V(vadas).next())", bindings)); // phantom referenced global closure bindings.put(GremlinGroovyScriptEngine.KEY_REFERENCE_TYPE, GremlinGroovyScriptEngine.REFERENCE_TYPE_PHANTOM); engine.eval("def isMarko(v){v.value('name')=='marko'}", bindings); try { engine.eval("isMarko(g.V(marko).next())", bindings); fail("the isMarko function should not be present"); } catch (Exception ex) { } assertEquals(true, engine.eval("def isMarko(v){v.value('name')=='marko'}; isMarko(g.V(marko).next())", bindings)); try { engine.eval("isMarko(g.V(marko" + ").next())", bindings); fail("the isMarko function should not be present"); } catch (Exception ex) { } bindings.remove(GremlinGroovyScriptEngine.KEY_REFERENCE_TYPE); // isVadas class was a hard reference so it should still be hanging about assertEquals(true, engine.eval("isVadas(g.V(vadas).next())", bindings)); } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldAllowFunctionsUsedInClosure() throws ScriptException { final GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("#jsr223.groovy.engine.keep.globals", "phantom"); bindings.put("vadas", convertToVertexId("vadas")); // this works on its own when the function and the line that uses it is in one "script". this is the // current workaround assertEquals(g.V(convertToVertexId("vadas")).next(), engine.eval("def isVadas(v){v.value('name')=='vadas'};g.V().filter{isVadas(it.get())}.next()", bindings)); // let's reset this piece and make sure isVadas is not hanging around. engine.reset(); // validate that isVadas throws an exception since it is not defined try { engine.eval("isVadas(g.V(vadas).next())", bindings); // fail the test if the above doesn't throw an exception fail(); } catch (Exception ex) { // this is good...we want this. it means isVadas isn't hanging about } // now...define the function separately on its own in one script bindings.remove("#jsr223.groovy.engine.keep.globals"); engine.eval("def isVadas(v){v.value('name')=='vadas'}", bindings); // make sure the function works on its own...no problem assertEquals(true, engine.eval("isVadas(g.V(vadas).next())", bindings)); // make sure the function works in a closure...this generates a StackOverflowError assertEquals(g.V(convertToVertexId("vadas")).next(), engine.eval("g.V().filter{isVadas(it.get())}.next()", bindings)); } @Test @org.junit.Ignore @LoadGraphWith(LoadGraphWith.GraphData.CLASSIC) public void shouldAllowUseOfClasses() throws ScriptException { GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); final Bindings bindings = engine.createBindings(); bindings.put("g", g); bindings.put("vadas", convertToVertexId("vadas")); // works when it's all defined together assertEquals(true, engine.eval("class c { static def isVadas(v){v.value('name')=='vadas'}};c.isVadas(g.V(vadas).next())", bindings)); // let's reset this piece and make sure isVadas is not hanging around. engine.reset(); // validate that isVadas throws an exception since it is not defined try { engine.eval("c.isVadas(g.V(vadas).next())", bindings); // fail the test if the above doesn't throw an exception fail("Function should be gone"); } catch (Exception ex) { // this is good...we want this. it means isVadas isn't hanging about } // now...define the class separately on its own in one script... // HERE'S an AWKWARD BIT......... // YOU HAVE TO END WITH: null; // ....OR ELSE YOU GET: // javax.script.ScriptException: javax.script.ScriptException: // org.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: c.main() // is applicable for argument types: ([Ljava.lang.String;) values: [[]] // WOULD BE NICE IF WE DIDN'T HAVE TO DO THAT engine.eval("class c { static def isVadas(v){v.name=='vadas'}};null;", bindings); // make sure the class works on its own...this generates: groovy.lang.MissingPropertyException: No such property: c for class: Script2 assertEquals(true, engine.eval("c.isVadas(g.V(vadas).next())", bindings)); } @Test @FeatureRequirementSet(FeatureRequirementSet.Package.VERTICES_ONLY) public void shouldProcessUTF8Query() throws Exception { final Vertex nonUtf8 = graph.addVertex("name", "marko", "age", 29); final Vertex utf8Name = graph.addVertex("name", "轉注", "age", 32); final ScriptEngine engine = new GremlinGroovyScriptEngine(); engine.put("g", g); Traversal eval = (Traversal) engine.eval("g.V().has('name', 'marko')"); assertEquals(nonUtf8, eval.next()); eval = (Traversal) engine.eval("g.V().has('name','轉注')"); assertEquals(utf8Name, eval.next()); } }