Skip to content

Commit

Permalink
Ref #4407: js-dsl - Improve the interoperability with Java code
Browse files Browse the repository at this point in the history
  • Loading branch information
essobedo committed Jan 16, 2023
1 parent 1200a9b commit 80f3e40
Show file tree
Hide file tree
Showing 26 changed files with 1,136 additions and 43 deletions.
38 changes: 38 additions & 0 deletions docs/modules/ROOT/pages/reference/extensions/js-dsl.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,41 @@ Please refer to the above link for usage and configuration details.
ifeval::[{doc-show-user-guide-link} == true]
Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.
endif::[]

[id="extensions-js-dsl-camel-quarkus-limitations"]
== Camel Quarkus limitations

Since the function `Java.extend` https://www.graalvm.org/latest/reference-manual/js/JavaInteroperability/#extending-java-classes[is only available in JVM mode], by default, there is no way to implement a functional interface like a `Camel Processor` in JavaScript that is supported by the native compilation.

As workaround, an implementation of the main functional interfaces (`org.apache.camel.Processor`, `java.util.function.Consumer`, `java.util.function.Supplier`, `java.util.function.Function`, `java.util.function.Predicate`, `java.util.function.BiConsumer`, `java.util.function.BiFunction` and `java.util.function.BiPredicate`) is available in the package `org.apache.camel.quarkus.dsl.js.runtime` whose simple name is prefixed by `JavaScriptDsl`. For each implementation, the body of the method to implement must be provided to the constructor. When the method to implement has arguments, the name of the arguments can also be provided to the constructor if the default names are not good enough.

So for example, to implement a `Camel Processor` instead of using the function `Java.extend` which is only available in JVM mode as next:

[source,javascript]
----
const Processor = Java.type("org.apache.camel.Processor"); // <1>
const p = Java.extend(Processor); // <2>
const a = new p(e => { e.getMessage().setBody('Some Content') }); // <3>
from('direct:a')
.process(a); // <4>
----
<1> Retrieve the class `org.apache.camel.Processor`
<2> Create a new class that implements the functional interface `org.apache.camel.Processor`
<3> Instantiate the new class with a function as argument representing the implementation of the method to implement
<4> Provide the processor to the route definition.

To have a code compatible with the both modes, it is possible to instantiate directly the implementation of the corresponding functional interface which is in this case the class `org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslProcessor` as next:

[source,javascript]
----
const Processor = Java.type("org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslProcessor"); // <1>
const p = new Processor("e", `e.getMessage().setBody('Some Content')`); // <2>
from('direct:a')
.process(p); // <3>
----
<1> Retrieve the class `org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslProcessor`
<2> Instantiate the dedicated class with the name of the argument `e` as first parameter and the body of the function as second parameter representing the implementation of the method to implement
<3> Provide the processor to the route definition.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.camel.quarkus.dsl.js.deployment;

import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.Temporal;
Expand All @@ -31,13 +32,21 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;

import io.quarkus.arc.Components;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.deployment.pkg.steps.NativeBuild;
import org.apache.camel.CamelContext;
import org.apache.camel.Component;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Message;
import org.apache.camel.NamedNode;
import org.apache.camel.builder.DataFormatClause;
import org.apache.camel.builder.ExpressionClause;
Expand All @@ -55,6 +64,13 @@
import org.apache.camel.model.rest.RestSecurityDefinition;
import org.apache.camel.model.transformer.TransformerDefinition;
import org.apache.camel.model.validator.ValidatorDefinition;
import org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslBiConsumer;
import org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslBiFunction;
import org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslBiPredicate;
import org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslConsumer;
import org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslFunction;
import org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslPredicate;
import org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslSupplier;
import org.apache.camel.spi.ExchangeFormatter;
import org.apache.camel.spi.NamespaceAware;
import org.jboss.jandex.ClassInfo;
Expand Down Expand Up @@ -93,6 +109,10 @@ public class JavaScriptDslProcessor {
ExpressionDefinition.class,
ExpressionClause.class,
Exchange.class,
Message.class,
ExchangePattern.class,
Endpoint.class,
CamelContext.class,
JsonLibrary.class,
NamedNode.class,
OptionalIdentifiedDefinition.class,
Expand All @@ -102,10 +122,12 @@ public class JavaScriptDslProcessor {
ValidatorDefinition.class,
TransformerDefinition.class,
NoOutputDefinition.class);
public static final String BUILDER_CLASS_SUFFIX = "Builders";

@BuildStep
@BuildStep(onlyIf = NativeBuild.class)
void registerReflectiveClasses(
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ReflectiveMethodBuildItem> reflectiveMethods,
CombinedIndexBuildItem combinedIndexBuildItem) {

IndexView view = combinedIndexBuildItem.getIndex();
Expand All @@ -131,7 +153,55 @@ void registerReflectiveClasses(
}

reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, Components.class));
reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, JavaScriptDSL.class));
reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, JavaScriptDSL.class));
reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, "org.apache.camel.converter.jaxp.XmlConverter"));

Set<String> existingComponents = view.getAllKnownImplementors(Component.class)
.stream()
.map(JavaScriptDslProcessor::extractName)
.collect(Collectors.toSet());

Set<Class<?>> types = new HashSet<>();
// Register all public methods of JavaScriptDSL for reflection to be accessible in native mode from a JavaScript resource
for (Method method : JavaScriptDSL.class.getMethods()) {
Class<?> declaringClass = method.getDeclaringClass();
if (!declaringClass.equals(Object.class)) {
String declaringClassName = declaringClass.getSimpleName();
// Keep only the methods that are not from builder classes or that are from builder classes of included
// components
if (!declaringClassName.endsWith(BUILDER_CLASS_SUFFIX) || existingComponents.contains(
declaringClassName.substring(0, declaringClassName.length() - BUILDER_CLASS_SUFFIX.length()))) {
types.add(method.getReturnType());
reflectiveMethods.produce(new ReflectiveMethodBuildItem(method));
}
}
}
// Register all the Camel return types of public methods of the camel reflective classes for reflection to
// be accessible in native mode from a JavaScript resource
for (Class<?> c : CAMEL_REFLECTIVE_CLASSES) {
for (Method method : c.getMethods()) {
if (!method.getDeclaringClass().equals(Object.class)) {
Class<?> returnType = method.getReturnType();
if (returnType.getPackageName().startsWith("org.apache.camel.")
&& !CAMEL_REFLECTIVE_CLASSES.contains(returnType)) {
types.add(returnType);
}
}
}
}
// Allow access to methods by reflection to be accessible in native mode from a JavaScript resource
reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, false, types.toArray(new Class<?>[0])));
// Register for reflection the runtime implementation of the main functional interfaces.
reflectiveClass.produce(
new ReflectiveClassBuildItem(false, false, JavaScriptDslBiConsumer.class, JavaScriptDslBiFunction.class,
JavaScriptDslBiPredicate.class, JavaScriptDslConsumer.class, JavaScriptDslFunction.class,
JavaScriptDslPredicate.class, org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslProcessor.class,
JavaScriptDslSupplier.class));
}

private static String extractName(ClassInfo classInfo) {
String className = classInfo.simpleName();
int index = className.lastIndexOf('.');
return className.substring(index + 1).replace("Component", "");
}
}
33 changes: 33 additions & 0 deletions extensions/js-dsl/runtime/src/main/doc/limitations.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Since the function `Java.extend` https://www.graalvm.org/latest/reference-manual/js/JavaInteroperability/#extending-java-classes[is only available in JVM mode], by default, there is no way to implement a functional interface like a `Camel Processor` in JavaScript that is supported by the native compilation.

As workaround, an implementation of the main functional interfaces (`org.apache.camel.Processor`, `java.util.function.Consumer`, `java.util.function.Supplier`, `java.util.function.Function`, `java.util.function.Predicate`, `java.util.function.BiConsumer`, `java.util.function.BiFunction` and `java.util.function.BiPredicate`) is available in the package `org.apache.camel.quarkus.dsl.js.runtime` whose simple name is prefixed by `JavaScriptDsl`. For each implementation, the body of the method to implement must be provided to the constructor. When the method to implement has arguments, the name of the arguments can also be provided to the constructor if the default names are not good enough.

So for example, to implement a `Camel Processor` instead of using the function `Java.extend` which is only available in JVM mode as next:

[source,javascript]
----
const Processor = Java.type("org.apache.camel.Processor"); // <1>
const p = Java.extend(Processor); // <2>
const a = new p(e => { e.getMessage().setBody('Some Content') }); // <3>
from('direct:a')
.process(a); // <4>
----
<1> Retrieve the class `org.apache.camel.Processor`
<2> Create a new class that implements the functional interface `org.apache.camel.Processor`
<3> Instantiate the new class with a function as argument representing the implementation of the method to implement
<4> Provide the processor to the route definition.

To have a code compatible with the both modes, it is possible to instantiate directly the implementation of the corresponding functional interface which is in this case the class `org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslProcessor` as next:

[source,javascript]
----
const Processor = Java.type("org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslProcessor"); // <1>
const p = new Processor("e", `e.getMessage().setBody('Some Content')`); // <2>
from('direct:a')
.process(p); // <3>
----
<1> Retrieve the class `org.apache.camel.quarkus.dsl.js.runtime.JavaScriptDslProcessor`
<2> Instantiate the dedicated class with the name of the argument `e` as first parameter and the body of the function as second parameter representing the implementation of the method to implement
<3> Provide the processor to the route definition.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.quarkus.dsl.js.runtime;

import java.util.function.BiConsumer;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

import static org.apache.camel.dsl.js.JavaScriptRoutesBuilderLoader.LANGUAGE_ID;

/**
* {@code JavaScriptDslBiConsumer} is meant to be used as type of {@link BiConsumer} from a JavaScript file to remain
* compatible
* with the native mode that doesn't support the function {@code Java.extend}.
*
* @param <T> the type of the first argument to the operation
* @param <U> the type of the second argument to the operation
*/
public final class JavaScriptDslBiConsumer<T, U> implements BiConsumer<T, U> {

/**
* The name of the first argument.
*/
private final String firstArgumentName;
/**
* The name of the second argument.
*/
private final String secondArgumentName;
/**
* The source of the consumer.
*/
private final CharSequence source;

/**
* Construct a {@code JavaScriptDslBiConsumer} with the given source, {@code t} as first argument name and {@code u} as
* second argument.
*
* @param source the source of the consumer.
*/
public JavaScriptDslBiConsumer(CharSequence source) {
this("t", "u", source);
}

/**
* Construct a {@code JavaScriptDslBiConsumer} with the given source and argument names.
*
* @param firstArgumentName the name of the first argument.
* @param secondArgumentName the name of the second argument.
* @param source the source of the consumer.
*/
public JavaScriptDslBiConsumer(String firstArgumentName, String secondArgumentName, CharSequence source) {
this.firstArgumentName = firstArgumentName;
this.secondArgumentName = secondArgumentName;
this.source = source;
}

@Override
public void accept(T t, U u) {
try (final Context context = JavaScriptDslHelper.createBuilder().build()) {
final Value bindings = context.getBindings(LANGUAGE_ID);
bindings.putMember(firstArgumentName, t);
bindings.putMember(secondArgumentName, u);
context.eval(LANGUAGE_ID, source);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.quarkus.dsl.js.runtime;

import java.util.function.BiFunction;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

import static org.apache.camel.dsl.js.JavaScriptRoutesBuilderLoader.LANGUAGE_ID;

/**
* {@code JavaScriptDslBiFunction} is meant to be used as type of {@link BiFunction} from a JavaScript file to remain
* compatible
* with the native mode that doesn't support the function {@code Java.extend}.
*
* @param <T> the type of the first argument to the function
* @param <U> the type of the second argument to the function
* @param <R> the type of the result of the function
*/
public final class JavaScriptDslBiFunction<T, U, R> implements BiFunction<T, U, R> {

/**
* The name of the first argument.
*/
private final String firstArgumentName;
/**
* The name of the second argument.
*/
private final String secondArgumentName;
/**
* The source of the function.
*/
private final CharSequence source;

/**
* Construct a {@code JavaScriptDslBiFunction} with the given source, {@code t} as first argument name and {@code u} as
* second argument.
*
* @param source the source of the function.
*/
public JavaScriptDslBiFunction(CharSequence source) {
this("t", "u", source);
}

/**
* Construct a {@code JavaScriptDslBiFunction} with the given source and argument names.
*
* @param firstArgumentName the name of the first argument.
* @param secondArgumentName the name of the second argument.
* @param source the source of the function.
*/
public JavaScriptDslBiFunction(String firstArgumentName, String secondArgumentName, CharSequence source) {
this.firstArgumentName = firstArgumentName;
this.secondArgumentName = secondArgumentName;
this.source = source;
}

@SuppressWarnings("unchecked")
@Override
public R apply(T t, U u) {
try (final Context context = JavaScriptDslHelper.createBuilder().build()) {
final Value bindings = context.getBindings(LANGUAGE_ID);
bindings.putMember(firstArgumentName, t);
bindings.putMember(secondArgumentName, u);
Value value = context.eval(LANGUAGE_ID, source);
return value == null ? null : (R) value.as(Object.class);
}
}
}

0 comments on commit 80f3e40

Please sign in to comment.