-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
loader(java): support for jshell scripts
- Loading branch information
1 parent
ab7f0dc
commit 4a6c2e0
Showing
10 changed files
with
321 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/Jsh.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/* | ||
* 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.camel.k.loader.jsh; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
import javax.script.ScriptException; | ||
|
||
import jdk.jshell.JShell; | ||
import jdk.jshell.Snippet; | ||
import jdk.jshell.SnippetEvent; | ||
import jdk.jshell.SourceCodeAnalysis; | ||
import org.apache.camel.util.ObjectHelper; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public final class Jsh { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(Jsh.class); | ||
private static final ThreadLocal<Map<String, Object>> BINDINGS = ThreadLocal.withInitial(ConcurrentHashMap::new); | ||
|
||
private Jsh() { | ||
// no-op | ||
} | ||
|
||
public static List<String> compile(JShell jshell, String script) throws ScriptException { | ||
List<String> snippets = new ArrayList<>(); | ||
|
||
while (!script.isEmpty()) { | ||
SourceCodeAnalysis.CompletionInfo ci = jshell.sourceCodeAnalysis().analyzeCompletion(script); | ||
if (!ci.completeness().isComplete()) { | ||
throw new ScriptException("Incomplete script:\n" + script); | ||
} | ||
|
||
snippets.add(ci.source()); | ||
|
||
script = ci.remaining(); | ||
} | ||
|
||
return snippets; | ||
} | ||
|
||
public static void setBinding(JShell jshell, String name, Object value) throws ScriptException { | ||
ObjectHelper.notNull(jshell, "jshell"); | ||
ObjectHelper.notNull(name, "name"); | ||
ObjectHelper.notNull(value, "value"); | ||
|
||
setBinding(jshell, name, value, value.getClass()); | ||
} | ||
|
||
public static <T> void setBinding(JShell jshell, String name, T value, Class<? extends T> type) throws ScriptException { | ||
ObjectHelper.notNull(jshell, "jshell"); | ||
ObjectHelper.notNull(name, "name"); | ||
ObjectHelper.notNull(value, "value"); | ||
ObjectHelper.notNull(type, "type"); | ||
|
||
setBinding(name, value); | ||
|
||
// As JShell leverages LocalExecutionControl as execution engine and thus JShell | ||
// runs in the current process it is possible to access to local classes, we use | ||
// such capability to inject bindings as variables. | ||
String snippet = String.format( | ||
"var %s = %s.getBinding(\"%s\", %s.class);", | ||
name, | ||
Jsh.class.getName(), | ||
name, | ||
type.getName()); | ||
|
||
eval(jshell, snippet); | ||
} | ||
|
||
public static Object getBinding(String name) { | ||
return BINDINGS.get().get(name); | ||
} | ||
|
||
public static <T> T getBinding(String name, Class<T> type) { | ||
Object answer = BINDINGS.get().get(name); | ||
return answer != null ? type.cast(answer) : null; | ||
} | ||
|
||
public static void setBinding(String name, Object value) { | ||
BINDINGS.get().put(name, value); | ||
} | ||
|
||
public static void clearBindings() { | ||
BINDINGS.get().clear(); | ||
} | ||
|
||
public static void eval(JShell jshell, String snippet) throws ScriptException { | ||
LOGGER.debug("Evaluating {}", snippet); | ||
|
||
List<SnippetEvent> events = jshell.eval(snippet); | ||
|
||
for (SnippetEvent event : events) { | ||
if (event.exception() != null) { | ||
throw new ScriptException(event.exception()); | ||
} | ||
if (event.status() != Snippet.Status.VALID) { | ||
throw new ScriptException("Error evaluating snippet:\n" + event.snippet().source()); | ||
} | ||
} | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/JshSourceLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* | ||
* 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.camel.k.loader.jsh; | ||
|
||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.Map; | ||
|
||
import javax.script.ScriptException; | ||
|
||
import jdk.jshell.JShell; | ||
import jdk.jshell.execution.DirectExecutionControl; | ||
import jdk.jshell.spi.ExecutionControl; | ||
import jdk.jshell.spi.ExecutionControlProvider; | ||
import jdk.jshell.spi.ExecutionEnv; | ||
import org.apache.camel.CamelContext; | ||
import org.apache.camel.Experimental; | ||
import org.apache.camel.RoutesBuilder; | ||
import org.apache.camel.builder.endpoint.EndpointRouteBuilder; | ||
import org.apache.camel.k.Source; | ||
import org.apache.camel.k.SourceLoader; | ||
import org.apache.camel.k.annotation.Loader; | ||
import org.apache.camel.k.support.RouteBuilders; | ||
import org.apache.camel.spi.Registry; | ||
import org.apache.camel.util.IOHelper; | ||
|
||
/** | ||
* A {@link SourceLoader} implementation based on {@link JShell}. | ||
*/ | ||
@Experimental | ||
@Loader(value = "jsh") | ||
public class JshSourceLoader implements SourceLoader { | ||
@Override | ||
public Collection<String> getSupportedLanguages() { | ||
return Collections.singletonList("jsh"); | ||
} | ||
|
||
@Override | ||
public RoutesBuilder load(CamelContext camelContext, Source source) { | ||
return RouteBuilders.endpoint( | ||
source, | ||
(reader, builder) -> { | ||
final String content = IOHelper.toString(reader); | ||
final ExecutionControlProvider provider = new ExecutionControlProvider() { | ||
private final ExecutionControl control = new DirectExecutionControl(); | ||
|
||
@Override | ||
public String name() { | ||
return "jsh-direct"; | ||
} | ||
|
||
@Override | ||
public ExecutionControl generate(ExecutionEnv env, Map<String, String> parameters) throws Throwable { | ||
return control; | ||
} | ||
}; | ||
|
||
// | ||
// Leverage DirectExecutionControl as execution engine to make JShell running | ||
// in the current process and give a chance to bind variables to the script. | ||
// | ||
try (JShell jshell = JShell.builder().executionEngine(provider, null).build()) { | ||
// | ||
// since we can't set a base class for the snippet as we do for other | ||
// languages (groovy, kotlin) we need to introduce a top level variable | ||
// that users need to use to access the RouteBuilder, like: | ||
// | ||
// builder.from("timer:tick") | ||
// .to("log:info") | ||
// | ||
// context and thus registry can easily be retrieved from the registered | ||
// variable `builder` but for a better UX, add them as top level vars. | ||
// | ||
Jsh.setBinding(jshell, "builder", builder, EndpointRouteBuilder.class); | ||
Jsh.setBinding(jshell, "context", builder.getContext(), CamelContext.class); | ||
Jsh.setBinding(jshell, "registry", builder.getContext().getRegistry(), Registry.class); | ||
|
||
for (String snippet : Jsh.compile(jshell, content)) { | ||
Jsh.eval(jshell, snippet); | ||
} | ||
} catch (ScriptException e) { | ||
throw new RuntimeException(e); | ||
} finally { | ||
// remove contextual bindings once the snippet has been evaluated | ||
Jsh.clearBindings(); | ||
} | ||
} | ||
); | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
...loader-java/impl/src/test/groovy/org/apache/camel/k/loader/jsh/JshSourceLoaderTest.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* 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.camel.k.loader.jsh | ||
|
||
|
||
import org.apache.camel.k.loader.java.support.TestRuntime | ||
import org.apache.camel.model.ProcessDefinition | ||
import org.apache.camel.model.ToDefinition | ||
import spock.lang.AutoCleanup | ||
import spock.lang.Specification | ||
|
||
class JshSourceLoaderTest extends Specification { | ||
@AutoCleanup | ||
def runtime = new TestRuntime() | ||
|
||
def "load"(location) { | ||
expect: | ||
runtime.loadRoutes(location) | ||
|
||
with(runtime.context.routeDefinitions) { | ||
it[0].input.endpointUri ==~ /timer:.*tick/ | ||
it[0].outputs[0] instanceof ProcessDefinition | ||
it[0].outputs[1] instanceof ToDefinition | ||
} | ||
where: | ||
location << [ | ||
"classpath:jsh/routes.jsh" | ||
] | ||
|
||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
camel-k-loader-java/impl/src/test/resources/jsh/routes.jsh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
builder.from("timer:tick") | ||
.process(e -> {}) | ||
.to("log:info"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
= Writing Integrations in Java | ||
|
||
[CAUTION] | ||
==== | ||
Support for JShell is highly experimental | ||
==== | ||
|
||
Using Java and JShell to write an integration to be deployed using Camel K is no different from defining your routing rules in Camel with the only difference that you do not need to implement or extend a RouteBuilder but you can access the current builder thx to the built-in `builder` variable. | ||
|
||
[source,java] | ||
.example.jsh | ||
---- | ||
builder.from("timer:tick") | ||
.setBody() | ||
.constant("Hello Camel K!") | ||
.to("log:info");- | ||
---- | ||
|
||
You can run it with the standard command: | ||
|
||
``` | ||
kamel run example.jsh | ||
``` | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters