Skip to content

Commit

Permalink
loader(java): support for jshell scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
lburgazzoli committed Jan 26, 2021
1 parent ab7f0dc commit 4a6c2e0
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
import org.apache.camel.util.IOHelper;
import org.joor.Reflect;

/**
* A {@link SourceLoader} implementation based on jOOR (https://github.com/jOOQ/jOOR)
* to compile Java source file at runtime.
*/
@Loader(value = "java")
public class JavaSourceLoader implements SourceLoader {
private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([a-zA-Z][\\.\\w]*)\\s*;.*$", Pattern.MULTILINE);
Expand Down
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());
}
}
}
}
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();
}
}
);
}
}
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 camel-k-loader-java/impl/src/test/resources/jsh/routes.jsh
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");
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</Appenders>

<Loggers>
<Logger name="org.apache.camel.k.loader.jsh.Jsh" level="DEBUG"/>
<Root level="INFO">
<!--<AppenderRef ref="STDOUT"/>-->
<AppenderRef ref="NONE"/>
Expand Down
1 change: 1 addition & 0 deletions docs/modules/languages/nav-languages.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
** xref:languages:kotlin.adoc[Kotlin]
** xref:languages:javascript.adoc[JavaScript]
** xref:languages:java.adoc[Java]
** xref:languages:jsh.adoc[Java (JShell)]
** xref:languages:xml.adoc[XML]
** xref:languages:yaml.adoc[YAML]
25 changes: 25 additions & 0 deletions docs/modules/languages/pages/jsh.adoc
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
```


1 change: 1 addition & 0 deletions docs/modules/languages/pages/languages.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Camel K supports multiple languages for writing integrations:
|=======================
| Language | Description
| xref:java.adoc[Java] | Integrations written in Java DSL are supported.
| xref:jsh.adoc[JShell] | Integrations written in Java DSL using JShell.
| xref:xml.adoc[XML] | Integrations written in plain XML DSL are supported (Spring XML with <beans> or Blueprint XML with <blueprint> not supported).
| xref:yaml.adoc[YAML] | Integrations written in YAML DSL are supported.
| xref:groovy.adoc[Groovy] | Groovy `.groovy` files are supported (experimental).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private static void processLoaders(CamelCatalogSpec.Builder specBuilder) {
specBuilder.putLoader(
"java",
CamelLoader.fromArtifact("org.apache.camel.k", "camel-k-loader-java")
.addLanguage("java")
.addLanguages("java", "jsh")
.putMetadata("native", "false")
.build()
);
Expand Down

0 comments on commit 4a6c2e0

Please sign in to comment.