TINKERPOP-1562 Initial effort to deprecate ScriptEngines. This opens up some options for how to move plugins up to gremlin-core (or deprecate them completely - not surehow that will flow yet). Anyway, this is a wip commit and a nice stop point.
Project: http://git-wip-us.apache.org/repos/asf/tinkerpop/repo Commit: http://git-wip-us.apache.org/repos/asf/tinkerpop/commit/aca81c01 Tree: http://git-wip-us.apache.org/repos/asf/tinkerpop/tree/aca81c01 Diff: http://git-wip-us.apache.org/repos/asf/tinkerpop/diff/aca81c01 Branch: refs/heads/TINKERPOP-1562 Commit: aca81c011c1d6e2df9be8a0b4085db6987a1839a Parents: ffd6543 Author: Stephen Mallette <sp...@genoprime.com> Authored: Thu Nov 17 16:47:31 2016 -0500 Committer: Stephen Mallette <sp...@genoprime.com> Committed: Thu Dec 1 06:41:41 2016 -0500 ---------------------------------------------------------------------- .../gremlin/jsr223/AbstractGremlinModule.java | 48 +++++ .../gremlin/jsr223/CoreGremlinModule.java | 43 ++++- .../gremlin/jsr223/ImportCustomizer.java | 4 + .../gremlin/jsr223/ImportGremlinModule.java | 174 +++++++++++++++++++ .../gremlin/jsr223/ScriptCustomizer.java | 56 ++++++ .../gremlin/jsr223/ScriptFileModule.java | 71 ++++++++ .../gremlin/jsr223/ImportGremlinModuleTest.java | 149 ++++++++++++++++ .../gremlin/groovy/engine/GremlinExecutor.java | 76 +++++++- .../gremlin/groovy/engine/ScriptEngines.java | 2 + .../tinkerpop/gremlin/server/Settings.java | 8 + .../server/util/ServerGremlinExecutor.java | 28 ++- 11 files changed, 648 insertions(+), 11 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/AbstractGremlinModule.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/AbstractGremlinModule.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/AbstractGremlinModule.java new file mode 100644 index 0000000..36104f6 --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/AbstractGremlinModule.java @@ -0,0 +1,48 @@ +/* + * 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.jsr223; + +import java.util.Optional; +import java.util.Set; + +/** + * @author Stephen Mallette (http://stephen.genoprime.com) + */ +public abstract class AbstractGremlinModule implements GremlinModule { + protected final String moduleName; + protected final Customizer[] customizers; + protected final Set<String> appliesTo; + + public AbstractGremlinModule(final String moduleName, final Set<String> appliesTo, final Customizer... customizers) { + this.moduleName = moduleName; + this.appliesTo = appliesTo; + this.customizers = customizers; + } + + @Override + public String getName() { + return moduleName; + } + + @Override + public Optional<Customizer[]> getCustomizers(final String scriptEngineName) { + return null == scriptEngineName || appliesTo.isEmpty() || appliesTo.contains(scriptEngineName) ? + Optional.of(customizers) : Optional.empty(); + } +} http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreGremlinModule.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreGremlinModule.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreGremlinModule.java index dfe9f93..f1fdbe4 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreGremlinModule.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/CoreGremlinModule.java @@ -18,6 +18,8 @@ */ package org.apache.tinkerpop.gremlin.jsr223; +import org.apache.tinkerpop.gremlin.util.CoreImports; + import java.util.Optional; /** @@ -25,21 +27,54 @@ import java.util.Optional; * * @author Stephen Mallette (http://stephen.genoprime.com) */ -public class CoreGremlinModule implements GremlinModule { +public final class CoreGremlinModule implements GremlinModule { + + private static final String MODULE_NAME = "tinkerpop.core"; + + private static final ImportCustomizer gremlinCore = ImportCustomizer.build() + .addClassImports(CoreImports.getClassImports()) + .addEnumImports(CoreImports.getEnumImports()) + .addMethodImports(CoreImports.getMethodImports()).create(); - private static final Optional<Customizer[]> CUSTOMIZERS = Optional.of(new Customizer[] { ImportCustomizer.GREMLIN_CORE }); + private static final Customizer[] customizers = new Customizer[] {gremlinCore}; + private static final Builder builder = new Builder(); + /** + * @deprecated As of 3.2.4, replaced by {@link #instance()} as this field will later become private. + */ + @Deprecated public static final CoreGremlinModule INSTANCE = new CoreGremlinModule(); private CoreGremlinModule() {} + public static CoreGremlinModule instance() { + return INSTANCE; + } + @Override public Optional<Customizer[]> getCustomizers(final String scriptEngineName) { - return CUSTOMIZERS; + return Optional.of(customizers); } @Override public String getName() { - return "tinkerpop.core"; + return MODULE_NAME; + } + + /** + * {@link GremlinModule} instances all use a builder pattern for instantiation via configuration. This method is + * just provided for consistency with that pattern. When instantiating programmatically, use {@link #instance()}. + */ + public static Builder build() { + return builder; + } + + public final static class Builder { + + private Builder() {} + + public CoreGremlinModule create() { + return instance(); + } } } http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportCustomizer.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportCustomizer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportCustomizer.java index 6070839..2f0e08c 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportCustomizer.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportCustomizer.java @@ -34,6 +34,10 @@ import java.util.Set; */ public class ImportCustomizer implements Customizer { + /** + * @deprecated As of release 3.2.4, not replaced. + */ + @Deprecated public static final ImportCustomizer GREMLIN_CORE = ImportCustomizer.build() .addClassImports(CoreImports.getClassImports()) .addEnumImports(CoreImports.getEnumImports()) http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModule.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModule.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModule.java new file mode 100644 index 0000000..9fcbbce --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModule.java @@ -0,0 +1,174 @@ +/* + * 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.jsr223; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A module that allows custom class, static method and enum imports (i.e. those that are statically defined by a + * module within itself). A user might utilize this class to supply their own imports. This module is not specific + * to any {@link GremlinScriptEngine} - the imports are supplied to all engines. + * + * @author Stephen Mallette (http://stephen.genoprime.com) + */ +public final class ImportGremlinModule extends AbstractGremlinModule { + private static final String MODULE_NAME = "tinkerpop.import"; + + private ImportGremlinModule(final Builder builder) { + super(MODULE_NAME, builder.appliesTo, ImportCustomizer.build() + .addClassImports(builder.classImports) + .addEnumImports(builder.enumImports) + .addMethodImports(builder.methodImports).create()); + } + + public static Builder build() { + return new Builder(); + } + + public static final class Builder { + private static final Pattern METHOD_PATTERN = Pattern.compile("(.*)#(.*)\\((.*)\\)"); + private static final Pattern ENUM_PATTERN = Pattern.compile("(.*)#(.*)"); + private Set<Class> classImports = new HashSet<>(); + private Set<Method> methodImports = new HashSet<>(); + private Set<Enum> enumImports = new HashSet<>(); + private final Set<String> appliesTo = new HashSet<>(); + + private Builder() {} + + /** + * The name of the {@link GremlinScriptEngine} that this module will apply to. Setting no values here will + * make the module available to all the engines. + */ + public Builder appliesTo(final Collection<String> scriptEngineName) { + this.appliesTo.addAll(scriptEngineName); + return this; + } + + public Builder classImports(final Collection<String> classes) { + for (String clazz : classes) { + try { + classImports.add(Class.forName(clazz)); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + return this; + } + + public Builder methodImports(final Collection<String> methods) { + for (String method : methods) { + try { + if (method.endsWith("#*")) { + final String classString = method.substring(0, method.length() - 2); + final Class<?> clazz = Class.forName(classString); + methodImports.addAll(allStaticMethods(clazz)); + } else { + final Matcher matcher = METHOD_PATTERN.matcher(method); + + if (!matcher.matches()) + throw new IllegalArgumentException(String.format("Could not read method descriptor - check format of: %s", method)); + + final String classString = matcher.group(1); + final String methodString = matcher.group(2); + final String argString = matcher.group(3); + final Class<?> clazz = Class.forName(classString); + methodImports.add(clazz.getMethod(methodString, parse(argString))); + } + } catch (IllegalArgumentException iae) { + throw iae; + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + return this; + } + + public Builder enumImports(final Collection<String> enums) { + for (String enumItem : enums) { + try { + if (enumItem.endsWith("#*")) { + final String classString = enumItem.substring(0, enumItem.length() - 2); + final Class<?> clazz = Class.forName(classString); + enumImports.addAll(allEnums(clazz)); + } else { + final Matcher matcher = ENUM_PATTERN.matcher(enumItem); + + if (!matcher.matches()) + throw new IllegalArgumentException(String.format("Could not read enum descriptor - check format of: %s", enumItem)); + + final String classString = matcher.group(1); + final String enumValString = matcher.group(2); + final Class<?> clazz = Class.forName(classString); + + Stream.of(clazz.getEnumConstants()) + .filter(e -> ((Enum) e).name().equals(enumValString)) + .findFirst().ifPresent(e -> enumImports.add((Enum) e)); + } + } catch (IllegalArgumentException iae) { + throw iae; + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + return this; + } + + public ImportGremlinModule create() { + if (enumImports.isEmpty() && classImports.isEmpty() && methodImports.isEmpty()) + throw new IllegalStateException("At least one import must be specified"); + + return new ImportGremlinModule(this); + } + + private static List<Enum> allEnums(final Class<?> clazz) { + return Stream.of(clazz.getEnumConstants()).map(e -> (Enum) e).collect(Collectors.toList()); + } + + private static List<Method> allStaticMethods(final Class<?> clazz) { + return Stream.of(clazz.getMethods()).filter(m -> Modifier.isStatic(m.getModifiers())).collect(Collectors.toList()); + } + + private static Class<?>[] parse(final String argString) { + if (null == argString || argString.isEmpty()) + return new Class<?>[0]; + + final List<String> args = Stream.of(argString.split(",")).map(String::trim).collect(Collectors.toList()); + final Class<?>[] classes = new Class<?>[args.size()]; + for (int ix = 0; ix < args.size(); ix++) { + try { + classes[ix] = Class.forName(args.get(ix)); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + return classes; + } + } +} http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptCustomizer.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptCustomizer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptCustomizer.java new file mode 100644 index 0000000..28603df --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptCustomizer.java @@ -0,0 +1,56 @@ +/* + * 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.jsr223; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Stephen Mallette (http://stephen.genoprime.com) + */ +public class ScriptCustomizer implements Customizer { + + private final Collection<List<String>> scripts; + + public ScriptCustomizer(final Set<File> files) { + this(files.stream().map(f -> { + try { + return Files.lines(f.toPath(), StandardCharsets.UTF_8).collect(Collectors.toList()); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + }).collect(Collectors.toList())); + } + + public ScriptCustomizer(final Collection<List<String>> scripts) { + this.scripts = scripts; + } + + public Collection<List<String>> scripts() { + return scripts; + } +} http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptFileModule.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptFileModule.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptFileModule.java new file mode 100644 index 0000000..b5c0d0c --- /dev/null +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/ScriptFileModule.java @@ -0,0 +1,71 @@ +/* + * 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.jsr223; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stephen Mallette (http://stephen.genoprime.com) + */ +public final class ScriptFileModule extends AbstractGremlinModule { + private static final String MODULE_NAME = "tinkerpop.script"; + + public ScriptFileModule(final Builder builder) { + super(MODULE_NAME, builder.appliesTo, new ScriptCustomizer(builder.files)); + } + + public static Builder build() { + return new Builder(); + } + + public static final class Builder { + + private final Set<String> appliesTo = new HashSet<>(); + private Set<File> files = new HashSet<>(); + + private Builder() {} + + /** + * The name of the {@link GremlinScriptEngine} that this module will apply to. Setting no values here will + * make the module available to all the engines. + */ + public Builder appliesTo(final Collection<String> scriptEngineName) { + this.appliesTo.addAll(scriptEngineName); + return this; + } + + public Builder files(final Set<String> files) { + for (String f : files) { + final File file = new File(f); + if (!file.exists()) throw new IllegalArgumentException(new FileNotFoundException(f)); + this.files.add(file); + } + + return this; + } + + public ScriptFileModule create() { + return new ScriptFileModule(this); + } + } +} http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModuleTest.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModuleTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModuleTest.java new file mode 100644 index 0000000..ce62357 --- /dev/null +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/jsr223/ImportGremlinModuleTest.java @@ -0,0 +1,149 @@ +/* + * 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.jsr223; + +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.io.IoCore; +import org.apache.tinkerpop.gremlin.util.Gremlin; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsCollectionContaining.hasItems; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author Stephen Mallette (http://stephen.genoprime.com) + */ +public class ImportGremlinModuleTest { + + @Test(expected = IllegalStateException.class) + public void shouldImportSomething() { + ImportGremlinModule.build().create(); + } + + @Test + public void shouldImportClass() { + final ImportGremlinModule module = ImportGremlinModule.build() + .classImports(Collections.singletonList(Graph.class.getCanonicalName())).create(); + + final ImportCustomizer customizer = (ImportCustomizer) module.getCustomizers().get()[0]; + assertEquals(1, module.getCustomizers().get().length); + assertThat(customizer.getClassImports(), hasItems(Graph.class)); + assertEquals(1, customizer.getClassImports().size()); + } + + @Test + public void shouldImportWildcardMethod() throws Exception { + final Method zeroArgs = Gremlin.class.getMethod("version"); + final ImportGremlinModule module = ImportGremlinModule.build() + .methodImports(Collections.singletonList(Gremlin.class.getCanonicalName() + "#*")).create(); + + final ImportCustomizer customizer = (ImportCustomizer) module.getCustomizers().get()[0]; + assertEquals(1, module.getCustomizers().get().length); + assertThat(customizer.getMethodImports(), hasItems(zeroArgs)); + + // will also have the static main() method + assertEquals(2, customizer.getMethodImports().size()); + } + + @Test + public void shouldImportZeroArgMethod() throws Exception { + final Method zeroArgs = Gremlin.class.getMethod("version"); + final ImportGremlinModule module = ImportGremlinModule.build() + .methodImports(Collections.singletonList(toMethodDescriptor(zeroArgs))).create(); + + final ImportCustomizer customizer = (ImportCustomizer) module.getCustomizers().get()[0]; + assertEquals(1, module.getCustomizers().get().length); + assertThat(customizer.getMethodImports(), hasItems(zeroArgs)); + assertEquals(1, customizer.getMethodImports().size()); + } + + @Test + public void shouldImportSingleArgMethod() throws Exception { + final Method singleArg = IoCore.class.getMethod("createIoBuilder", String.class); + final ImportGremlinModule module = ImportGremlinModule.build() + .methodImports(Collections.singletonList(toMethodDescriptor(singleArg))).create(); + + final ImportCustomizer customizer = (ImportCustomizer) module.getCustomizers().get()[0]; + assertEquals(1, module.getCustomizers().get().length); + assertThat(customizer.getMethodImports(), hasItems(singleArg)); + assertEquals(1, customizer.getMethodImports().size()); + } + + @Test + public void shouldThrowExceptionIfInvalidMethodDescriptor() throws Exception { + final String badDescriptor = "Gremlin*version"; + try { + ImportGremlinModule.build() + .methodImports(Collections.singletonList(badDescriptor)).create(); + fail("Should have failed parsing the method descriptor"); + } catch (IllegalArgumentException iae) { + assertEquals(iae.getMessage(), "Could not read method descriptor - check format of: " + badDescriptor); + } + } + + @Test + public void shouldImportWildcardEnum() throws Exception { + final ImportGremlinModule module = ImportGremlinModule.build() + .enumImports(Collections.singletonList(T.class.getCanonicalName() + "#*")).create(); + + final ImportCustomizer customizer = (ImportCustomizer) module.getCustomizers().get()[0]; + assertEquals(1, module.getCustomizers().get().length); + assertThat(customizer.getEnumImports(), hasItems(T.id, T.key, T.label, T.value)); + assertEquals(4, customizer.getEnumImports().size()); + } + + @Test + public void shouldImportEnum() throws Exception { + final ImportGremlinModule module = ImportGremlinModule.build() + .enumImports(Collections.singletonList(T.class.getCanonicalName() + "#" + T.id.name())).create(); + + final ImportCustomizer customizer = (ImportCustomizer) module.getCustomizers().get()[0]; + assertEquals(1, module.getCustomizers().get().length); + assertThat(customizer.getEnumImports(), hasItems(T.id)); + } + + @Test + public void shouldThrowExceptionIfInvalidEnumDescriptor() throws Exception { + final String badDescriptor = "T*id"; + try { + ImportGremlinModule.build() + .enumImports(Collections.singletonList(badDescriptor)).create(); + fail("Should have failed parsing the enum descriptor"); + } catch (IllegalArgumentException iae) { + assertEquals("Could not read enum descriptor - check format of: " + badDescriptor, iae.getMessage()); + } + } + + private static String toMethodDescriptor(final Method method) { + return method.getDeclaringClass().getCanonicalName() + + "#" + + method.getName() + + '(' + + String.join(",", Stream.of(method.getParameters()).map(p -> p.getType().getCanonicalName()).collect(Collectors.toList())) + + ')'; + } +} http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java ---------------------------------------------------------------------- diff --git a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java index cc30f99..90a3b43 100644 --- a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java +++ b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java @@ -22,6 +22,9 @@ import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine; import org.apache.tinkerpop.gremlin.groovy.plugin.GremlinPlugin; import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.apache.tinkerpop.gremlin.jsr223.CachedGremlinScriptEngineManager; +import org.apache.tinkerpop.gremlin.jsr223.GremlinModule; +import org.apache.tinkerpop.gremlin.jsr223.GremlinScriptEngineManager; import org.javatuples.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +36,7 @@ import javax.script.SimpleBindings; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -51,11 +55,11 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Execute Gremlin scripts against a {@code ScriptEngine} instance. It is designed to host any JSR-223 enabled @@ -79,7 +83,10 @@ public class GremlinExecutor implements AutoCloseable { */ private ScriptEngines scriptEngines; + private GremlinScriptEngineManager manager; + private final Map<String, EngineSettings> settings; + private final Map<String, Map<String, Map<String,Object>>> modules; private final long scriptEvaluationTimeout; private final Bindings globalBindings; private final List<List<String>> use; @@ -92,6 +99,7 @@ public class GremlinExecutor implements AutoCloseable { private final Set<String> enabledPlugins; private final boolean suppliedExecutor; private final boolean suppliedScheduledExecutor; + private boolean useGremlinScriptEngineManager; private GremlinExecutor(final Builder builder, final boolean suppliedExecutor, final boolean suppliedScheduledExecutor) { @@ -104,10 +112,22 @@ public class GremlinExecutor implements AutoCloseable { this.afterFailure = builder.afterFailure; this.use = builder.use; this.settings = builder.settings; + this.modules = builder.modules; this.scriptEvaluationTimeout = builder.scriptEvaluationTimeout; this.globalBindings = builder.globalBindings; this.enabledPlugins = builder.enabledPlugins; - this.scriptEngines = createScriptEngines(); + + this.manager = new CachedGremlinScriptEngineManager(); + initializeGremlinScriptEngineManager(); + + // this is temporary so that we can have backward compatibilty to the old plugin system and ScriptEngines + // approach to configuring Gremlin Server and GremlinExecutor. This code/check should be removed with the + // deprecated code around this is removed. + if (!useGremlinScriptEngineManager) + this.scriptEngines = createScriptEngines(); + else + this.scriptEngines = null; + this.suppliedExecutor = suppliedExecutor; this.suppliedScheduledExecutor = suppliedScheduledExecutor; } @@ -284,7 +304,9 @@ public class GremlinExecutor implements AutoCloseable { logger.debug("Evaluating script - {} - in thread [{}]", script, Thread.currentThread().getName()); - final Object o = scriptEngines.eval(script, bindings, lang); + // do this weirdo check until the now deprecated ScriptEngines is gutted + final Object o = useGremlinScriptEngineManager ? + manager.getEngineByName(lang).eval(script, bindings) : scriptEngines.eval(script, bindings, lang); // apply a transformation before sending back the result - useful when trying to force serialization // in the same thread that the eval took place given ThreadLocal nature of graphs as well as some @@ -396,6 +418,46 @@ public class GremlinExecutor implements AutoCloseable { return future; } + private void initializeGremlinScriptEngineManager() { + this.useGremlinScriptEngineManager = !modules.entrySet().isEmpty(); + + for (Map.Entry<String, Map<String, Map<String,Object>>> config : modules.entrySet()) { + final String language = config.getKey(); + final Map<String, Map<String,Object>> moduleConfigs = config.getValue(); + for (Map.Entry<String, Map<String,Object>> moduleConfig : moduleConfigs.entrySet()) { + try { + final Class<?> clazz = Class.forName(moduleConfig.getKey()); + final Method builderMethod = clazz.getMethod("build"); + Object moduleBuilder = builderMethod.invoke(null); + + final Class<?> builderClazz = moduleBuilder.getClass(); + final Map<String,Object> customizerConfigs = moduleConfig.getValue(); + final Method[] methods = builderClazz.getMethods(); + for (Map.Entry<String,Object> customizerConfig : customizerConfigs.entrySet()) { + final Method configMethod = Stream.of(methods).filter(m -> m.getName().equals(customizerConfig.getKey())).findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find builder method on " + builderClazz.getCanonicalName())); + if (null == customizerConfig.getValue()) + moduleBuilder = configMethod.invoke(moduleBuilder); + else + moduleBuilder = configMethod.invoke(moduleBuilder, customizerConfig.getValue()); + } + + try { + final Method appliesTo = builderClazz.getMethod("appliesTo"); + moduleBuilder = appliesTo.invoke(moduleBuilder, language); + } catch (NoSuchMethodException ignored) { + + } + + final Method create = builderClazz.getMethod("create"); + manager.addModule((GremlinModule) create.invoke(moduleBuilder)); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + } + private ScriptEngines createScriptEngines() { // plugins already on the path - ones static to the classpath final List<GremlinPlugin> globalPlugins = new ArrayList<>(); @@ -490,6 +552,9 @@ public class GremlinExecutor implements AutoCloseable { public final static class Builder { private long scriptEvaluationTimeout = 8000; private Map<String, EngineSettings> settings = new HashMap<>(); + + private Map<String, Map<String, Map<String,Object>>> modules = new HashMap<>(); + private ExecutorService executorService = null; private ScheduledExecutorService scheduledExecutorService = null; private Set<String> enabledPlugins = new HashSet<>(); @@ -528,6 +593,11 @@ public class GremlinExecutor implements AutoCloseable { return this; } + public Builder addModules(final String engineName, final Map<String, Map<String,Object>> modules) { + this.modules.put(engineName, modules); + return this; + } + /** * Bindings to apply to every script evaluated. Note that the entries of the supplied {@code Bindings} object * will be copied into a newly created {@link org.apache.tinkerpop.gremlin.jsr223.ConcurrentBindings} object http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/ScriptEngines.java ---------------------------------------------------------------------- diff --git a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/ScriptEngines.java b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/ScriptEngines.java index 3984dbb..6911419 100644 --- a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/ScriptEngines.java +++ b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/ScriptEngines.java @@ -64,7 +64,9 @@ import java.util.stream.Stream; * Holds a batch of the configured {@code ScriptEngine} objects for the server. * * @author Stephen Mallette (http://stephen.genoprime.com) + * @deprecated As of release 3.2.4, not directly replaced - see {@link GremlinScriptEngineManager}. */ +@Deprecated public class ScriptEngines implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(ScriptEngines.class); http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java ---------------------------------------------------------------------- diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java index 4bb2089..a8395bb 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java @@ -20,6 +20,8 @@ package org.apache.tinkerpop.gremlin.server; import io.netty.handler.ssl.SslContext; import org.apache.tinkerpop.gremlin.driver.MessageSerializer; +import org.apache.tinkerpop.gremlin.jsr223.GremlinModule; +import org.apache.tinkerpop.gremlin.jsr223.GremlinScriptEngine; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator; @@ -258,6 +260,7 @@ public class Settings { scriptEngineSettingsDescription.putListPropertyType("staticImports", String.class); scriptEngineSettingsDescription.putListPropertyType("scripts", String.class); scriptEngineSettingsDescription.putMapPropertyType("config", String.class, Object.class); + scriptEngineSettingsDescription.putMapPropertyType("modules", String.class, Object.class); constructor.addTypeDescription(scriptEngineSettingsDescription); final TypeDescription sslSettings = new TypeDescription(SslSettings.class); @@ -336,6 +339,11 @@ public class Settings { * {@code ScriptEngine} implementation being used. */ public Map<String, Object> config = null; + + /** + * A set of configurations for {@link GremlinModule} instances to apply to this {@link GremlinScriptEngine}. + */ + public Map<String,Map<String,Object>> modules = new HashMap<>(); } /** http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/aca81c01/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java ---------------------------------------------------------------------- diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java index c8870ed..eda1d8d 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java @@ -19,6 +19,7 @@ package org.apache.tinkerpop.gremlin.server.util; import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor; +import org.apache.tinkerpop.gremlin.jsr223.ImportGremlinModule; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.server.Channelizer; import org.apache.tinkerpop.gremlin.server.GraphManager; @@ -28,7 +29,9 @@ import org.apache.tinkerpop.gremlin.structure.Graph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -127,10 +130,27 @@ public class ServerGremlinExecutor<T extends ScheduledExecutorService> { .scheduledExecutorService(this.scheduledExecutorService); settings.scriptEngines.forEach((k, v) -> { - // make sure that server related classes are available at init - v.imports.add(LifeCycleHook.class.getCanonicalName()); - v.imports.add(LifeCycleHook.Context.class.getCanonicalName()); - gremlinExecutorBuilder.addEngineSettings(k, v.imports, v.staticImports, v.scripts, v.config); + // use modules if they are present and the old approach if not + if (v.modules.isEmpty()) { + // make sure that server related classes are available at init - ultimately this body of code will be + // deleted when deprecation is removed + v.imports.add(LifeCycleHook.class.getCanonicalName()); + v.imports.add(LifeCycleHook.Context.class.getCanonicalName()); + gremlinExecutorBuilder.addEngineSettings(k, v.imports, v.staticImports, v.scripts, v.config); + } else { + // make sure that server related classes are available at init - ultimately this is the right way to + // do things going forward. + // TODO: though this Import is kinda sketchy. + if (v.modules.containsKey(ImportGremlinModule.class.getName())) { + final List<String> listToAddImportsTo = (List<String>) v.modules.get(ImportGremlinModule.class.getName()).get("classImports"); + listToAddImportsTo.addAll(Arrays.asList(LifeCycleHook.class.getName(), LifeCycleHook.Context.class.getName())); + } else { + final Map<String,Object> imports = new HashMap<>(); + imports.put("classImports", Arrays.asList(LifeCycleHook.class.getName(), LifeCycleHook.Context.class.getName())); + v.modules.put(ImportGremlinModule.class.getName(), imports); + } + gremlinExecutorBuilder.addModules(k, v.modules); + } }); gremlinExecutor = gremlinExecutorBuilder.create();