You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gi...@apache.org on 2020/12/12 03:29:44 UTC
[camel-quarkus] 06/09: CSimple language support #2036
This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch camel-master
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 100d9665c07d53f42226a56c78f63a83e60c51a9
Author: Peter Palaga <pp...@redhat.com>
AuthorDate: Thu Dec 3 12:45:19 2020 +0100
CSimple language support #2036
---
.../ROOT/pages/reference/extensions/core.adoc | 6 +
.../partials/reference/components/vertx-kafka.adoc | 1 +
extensions-core/core/deployment/pom.xml | 16 +
.../CSimpleRouteDefinitionProcessor.java | 354 +++++++++++++++++++++
.../LanguageExpressionContentHandler.java | 122 +++++++
.../spi/CSimpleExpressionSourceBuildItem.java | 58 ++++
.../spi/CompiledCSimpleExpressionBuildItem.java | 57 ++++
.../core/runtime/src/main/adoc/limitations.adoc | 17 +
.../quarkus/core/CSimpleLanguageRecorder.java | 24 +-
.../org/apache/camel/quarkus/core/CamelConfig.java | 18 ++
.../main/deployment/CSimpleXmlProcessor.java | 127 ++++++++
integration-tests/core/pom.xml | 42 +++
.../apache/camel/quarkus/core/CoreResource.java | 14 +
.../org/apache/camel/quarkus/core/CoreRoutes.java | 3 +
.../org/apache/camel/quarkus/core/CoreTest.java | 12 +
.../camel/quarkus/main/CoreMainXmlIoResource.java | 8 +
.../src/main/resources/routes/my-routes.xml | 7 +
.../camel/quarkus/main/CoreMainXmlIoTest.java | 13 +-
.../apache/camel/quarkus/main/CoreMainTest.java | 3 +-
19 files changed, 892 insertions(+), 10 deletions(-)
diff --git a/docs/modules/ROOT/pages/reference/extensions/core.adoc b/docs/modules/ROOT/pages/reference/extensions/core.adoc
index cd8e0a5..381b7b6 100644
--- a/docs/modules/ROOT/pages/reference/extensions/core.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/core.adoc
@@ -160,6 +160,12 @@ A comma separated list of Ant-path style patterns to match class names that shou
For this option to work properly, the artifacts containing the selected classes must either contain a Jandex index (`META-INF/jandex.idx`) or they must be registered for indexing using the `quarkus.index-dependency.++*++` family of options in `application.properties` - e.g. quarkus.index-dependency.my-dep.group-id = org.my-group quarkus.index-dependency.my-dep.artifact-id = my-artifact where `my-dep` is a label of your choice to tell Quarkus that `org.my-group` and with `my-artifact` b [...]
| `string`
|
+
+|icon:lock[title=Fixed at build time] [[quarkus.camel.csimple.on-build-time-analysis-failure]]`link:#quarkus.camel.csimple.on-build-time-analysis-failure[quarkus.camel.csimple.on-build-time-analysis-failure]`
+
+What to do if it is not possible to extract CSimple expressions from a route definition at build time.
+| `org.apache.camel.quarkus.core.CamelConfig.FailureRemedy`
+| `warn`
|===
[.configuration-legend]
diff --git a/docs/modules/ROOT/partials/reference/components/vertx-kafka.adoc b/docs/modules/ROOT/partials/reference/components/vertx-kafka.adoc
new file mode 100644
index 0000000..a509c1d
--- /dev/null
+++ b/docs/modules/ROOT/partials/reference/components/vertx-kafka.adoc
@@ -0,0 +1 @@
+// Empty partial for a Camel bit unsupported by Camel Quarkus to avoid warnings when this file is included from a Camel page
diff --git a/extensions-core/core/deployment/pom.xml b/extensions-core/core/deployment/pom.xml
index 37b0c2d..bf0822b 100644
--- a/extensions-core/core/deployment/pom.xml
+++ b/extensions-core/core/deployment/pom.xml
@@ -49,6 +49,22 @@
<artifactId>camel-quarkus-core</artifactId>
</dependency>
+ <!-- JAXB is needed for the build time routes introspection -->
+ <dependency>
+ <groupId>org.glassfish.jaxb</groupId>
+ <artifactId>jaxb-runtime</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>jakarta.xml.bind</groupId>
+ <artifactId>jakarta.xml.bind-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.spec.javax.xml.bind</groupId>
+ <artifactId>jboss-jaxb-api_2.3_spec</artifactId>
+ </dependency>
+
<!-- test dependencies -->
<dependency>
<groupId>org.apache.camel</groupId>
diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CSimpleRouteDefinitionProcessor.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CSimpleRouteDefinitionProcessor.java
new file mode 100644
index 0000000..c9878ab
--- /dev/null
+++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CSimpleRouteDefinitionProcessor.java
@@ -0,0 +1,354 @@
+/*
+ * 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.core.deployment;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Marshaller.Listener;
+
+import io.quarkus.bootstrap.classloading.ClassPathElement;
+import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.ExecutionTime;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
+import io.quarkus.deployment.dev.CompilationProvider;
+import io.quarkus.deployment.dev.CompilationProvider.Context;
+import io.quarkus.deployment.dev.JavaCompilationProvider;
+import io.quarkus.deployment.recording.RecorderContext;
+import io.quarkus.runtime.RuntimeValue;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.NamedNode;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.language.csimple.CSimpleCodeGenerator;
+import org.apache.camel.language.csimple.CSimpleGeneratedCode;
+import org.apache.camel.language.csimple.CSimpleHelper;
+import org.apache.camel.language.csimple.CSimpleLanguage;
+import org.apache.camel.language.csimple.CSimpleLanguage.Builder;
+import org.apache.camel.model.Constants;
+import org.apache.camel.model.ExpressionNode;
+import org.apache.camel.quarkus.core.CSimpleLanguageRecorder;
+import org.apache.camel.quarkus.core.CamelConfig;
+import org.apache.camel.quarkus.core.CamelConfig.FailureRemedy;
+import org.apache.camel.quarkus.core.deployment.spi.CSimpleExpressionSourceBuildItem;
+import org.apache.camel.quarkus.core.deployment.spi.CamelBeanBuildItem;
+import org.apache.camel.quarkus.core.deployment.spi.CamelContextBuildItem;
+import org.apache.camel.quarkus.core.deployment.spi.CamelRoutesBuilderClassBuildItem;
+import org.apache.camel.quarkus.core.deployment.spi.CompiledCSimpleExpressionBuildItem;
+import org.apache.camel.util.PropertiesHelper;
+import org.jboss.logging.Logger;
+
+class CSimpleRouteDefinitionProcessor {
+ private static final Logger LOG = Logger.getLogger(CSimpleRouteDefinitionProcessor.class);
+ static final String CLASS_EXT = ".class";
+
+ @BuildStep
+ void collectCSimpleExpresions(
+ CamelConfig config,
+ List<CamelRoutesBuilderClassBuildItem> routesBuilderClasses,
+ BuildProducer<CSimpleExpressionSourceBuildItem> csimpleExpressions)
+ throws IOException, ClassNotFoundException, URISyntaxException, JAXBException {
+
+ if (!routesBuilderClasses.isEmpty()) {
+ final ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ if (!(loader instanceof QuarkusClassLoader)) {
+ throw new IllegalStateException(
+ QuarkusClassLoader.class.getSimpleName() + " expected as the context class loader");
+ }
+
+ final ExpressionCollector collector = new ExpressionCollector(loader);
+
+ final CamelContext ctx = new DefaultCamelContext();
+ for (CamelRoutesBuilderClassBuildItem routesBuilderClass : routesBuilderClasses) {
+ final String className = routesBuilderClass.getDotName().toString();
+ final Class<?> cl = loader.loadClass(className);
+
+ if (!RouteBuilder.class.isAssignableFrom(cl)) {
+ LOG.warnf("CSimple language expressions ocurring in %s won't be compiled at build time", cl);
+ } else {
+ try {
+ final RouteBuilder rb = (RouteBuilder) cl.newInstance();
+ rb.setContext(ctx);
+ try {
+ rb.configure();
+ collector.collect(
+ "csimple",
+ (script, isPredicate) -> csimpleExpressions.produce(
+ new CSimpleExpressionSourceBuildItem(
+ script,
+ isPredicate,
+ className)),
+ rb.getRouteCollection(),
+ rb.getRestCollection());
+
+ } catch (Exception e) {
+ switch (config.csimple.onBuildTimeAnalysisFailure) {
+ case fail:
+ throw new RuntimeException(
+ "Could not extract CSimple expressions from " + className
+ + ". You may want to set quarkus.camel.csimple.on-build-time-analysis-failure to warn or ignore if you do not use CSimple language in your routes",
+ e);
+ case warn:
+ LOG.warnf(e,
+ "Could not extract CSimple language expressions from the route definition %s in class %s.",
+ rb, cl);
+ break;
+ case ignore:
+ LOG.debugf(e,
+ "Could not extract CSimple language expressions from the route definition %s in class %s",
+ rb, cl);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected " + FailureRemedy.class.getSimpleName() + ": "
+ + config.csimple.onBuildTimeAnalysisFailure);
+ }
+ }
+
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException("Could not instantiate " + className, e);
+ }
+ }
+ }
+ }
+ }
+
+ @BuildStep
+ void compileCSimpleExpresions(
+ List<CSimpleExpressionSourceBuildItem> expressionSources,
+ BuildProducer<CompiledCSimpleExpressionBuildItem> compiledCSimpleExpression,
+ BuildProducer<GeneratedClassBuildItem> generatedClasses) throws IOException {
+
+ if (!expressionSources.isEmpty()) {
+ final Set<String> imports = new TreeSet<>();
+ final Map<String, String> aliases = new LinkedHashMap<>();
+ final ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ if (!(loader instanceof QuarkusClassLoader)) {
+ throw new IllegalStateException(
+ QuarkusClassLoader.class.getSimpleName() + " expected as the context class loader");
+ }
+ final QuarkusClassLoader quarkusClassLoader = (QuarkusClassLoader) loader;
+ readConfig(imports, aliases, loader);
+ final CSimpleCodeGenerator generator = new CSimpleCodeGenerator();
+ generator.setAliases(aliases);
+ generator.setImports(imports);
+
+ final Path projectDir = Paths.get(".").toAbsolutePath().normalize();
+ final Path csimpleGeneratedSourceDir = projectDir.resolve("target/generated/csimple");
+ Files.createDirectories(csimpleGeneratedSourceDir);
+
+ final Set<File> filesToCompile = new LinkedHashSet<>();
+
+ /* We do not want to compile the same source twice, so we store here what we have compiled already */
+ final Map<Boolean, Set<String>> compiledExpressions = new HashMap<>();
+ compiledExpressions.put(true, new HashSet<>());
+ compiledExpressions.put(false, new HashSet<>());
+
+ /* Generate Java classes for the language expressions */
+ for (CSimpleExpressionSourceBuildItem expr : expressionSources) {
+ final boolean predicate = expr.isPredicate();
+ final String script = expr.getSourceCode();
+ if (!compiledExpressions.get(predicate).contains(script)) {
+ final CSimpleGeneratedCode code = predicate
+ ? generator.generatePredicate(expr.getClassNameBase(), script)
+ : generator.generateExpression(expr.getClassNameBase(), script);
+
+ compiledCSimpleExpression
+ .produce(new CompiledCSimpleExpressionBuildItem(code.getCode(), predicate, code.getFqn()));
+
+ final Path javaCsimpleFile = csimpleGeneratedSourceDir
+ .resolve(code.getFqn().replace('.', '/') + ".java");
+ Files.createDirectories(javaCsimpleFile.getParent());
+ Files.write(javaCsimpleFile, code.getCode().getBytes(StandardCharsets.UTF_8));
+ filesToCompile.add(javaCsimpleFile.toFile());
+ compiledExpressions.get(predicate).add(script);
+ }
+ }
+
+ final Path csimpleClassesDir = projectDir.resolve("target/csimple-classes");
+ Files.createDirectories(csimpleClassesDir);
+
+ /* Compile the generated sources */
+ try (JavaCompilationProvider compiler = new JavaCompilationProvider()) {
+ final Context context = compilationContext(projectDir, csimpleClassesDir, quarkusClassLoader);
+ compiler.compile(filesToCompile, context);
+ }
+
+ /* Register the compiled classes via Quarkus GeneratedClassBuildItem */
+ try (Stream<Path> classFiles = Files.walk(csimpleClassesDir)) {
+ classFiles
+ .filter(Files::isRegularFile)
+ .filter(p -> p.getFileName().toString().endsWith(CLASS_EXT))
+ .forEach(p -> {
+ final Path relPath = csimpleClassesDir.relativize(p);
+ String className = relPath.toString();
+ className = className.substring(0, className.length() - CLASS_EXT.length());
+ try {
+ final GeneratedClassBuildItem item = new GeneratedClassBuildItem(true, className,
+ Files.readAllBytes(p));
+ generatedClasses.produce(item);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read " + p);
+ }
+ });
+ }
+
+ }
+ }
+
+ @Record(ExecutionTime.STATIC_INIT)
+ @BuildStep
+ CamelBeanBuildItem configureCSimpleLanguage(
+ RecorderContext recorderContext,
+ CSimpleLanguageRecorder recorder,
+ CamelContextBuildItem camelContext,
+ List<CompiledCSimpleExpressionBuildItem> compiledCSimpleExpressions) {
+
+ final RuntimeValue<Builder> builder = recorder.csimpleLanguageBuilder();
+ for (CompiledCSimpleExpressionBuildItem expr : compiledCSimpleExpressions) {
+ recorder.addExpression(builder, recorderContext.newInstance(expr.getClassName()));
+ }
+
+ final RuntimeValue<?> csimpleLanguage = recorder.buildCSimpleLanguage(builder);
+ return new CamelBeanBuildItem("csimple", CSimpleLanguage.class.getName(), csimpleLanguage);
+ }
+
+ static void readConfig(Set<String> imports, Map<String, String> aliases, ClassLoader cl) throws IOException {
+ Enumeration<URL> confiUrls = cl.getResources("camel-csimple.properties");
+ while (confiUrls.hasMoreElements()) {
+ final URL configUrl = confiUrls.nextElement();
+ try (BufferedReader r = new BufferedReader(new InputStreamReader(configUrl.openStream(), StandardCharsets.UTF_8))) {
+ String line = null;
+ while ((line = r.readLine()) != null) {
+ line = line.trim();
+ // skip comments
+ if (line.startsWith("#")) {
+ continue;
+ }
+ // imports
+ if (line.startsWith("import ")) {
+ imports.add(line);
+ continue;
+ }
+ // aliases as key=value
+ final int eqPos = line.indexOf('=');
+ final String key = line.substring(0, eqPos).trim();
+ final String value = line.substring(eqPos + 1).trim();
+ aliases.put(key, value);
+
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read from " + configUrl);
+ }
+ }
+ }
+
+ private Context compilationContext(final Path projectDir, final Path csimpleClassesDir,
+ QuarkusClassLoader quarkusClassLoader) {
+ Set<File> classPathElements = Stream.of(CSimpleHelper.class, Exchange.class, PropertiesHelper.class)
+ .map(clazz -> clazz.getName().replace('.', '/') + CLASS_EXT)
+ .flatMap(className -> (Stream<ClassPathElement>) quarkusClassLoader.getElementsWithResource(className).stream())
+ .map(cpe -> cpe.getRoot())
+ .filter(p -> p != null)
+ .map(Path::toFile)
+ .collect(Collectors.toSet());
+
+ return new CompilationProvider.Context(
+ "csimple-project",
+ classPathElements,
+ projectDir.toFile(),
+ projectDir.resolve("src/main/java").toFile(),
+ csimpleClassesDir.toFile(),
+ StandardCharsets.UTF_8.name(),
+ Collections.emptyList(),
+ "1.8",
+ "1.8",
+ Collections.emptyList(),
+ Collections.emptyList());
+ }
+
+ /**
+ * Collects expressions of a given language.
+ */
+ static class ExpressionCollector {
+ private final JAXBContext jaxbContext;
+ private final Marshaller marshaler;
+
+ ExpressionCollector(ClassLoader loader) {
+ try {
+ jaxbContext = JAXBContext.newInstance(Constants.JAXB_CONTEXT_PACKAGES, loader);
+ Marshaller m = jaxbContext.createMarshaller();
+ m.setListener(new RouteDefinitionNormalizer());
+ marshaler = m;
+ } catch (JAXBException e) {
+ throw new RuntimeException("Could not creat a JAXB marshaler", e);
+ }
+ }
+
+ public void collect(String languageName, BiConsumer<String, Boolean> expressionConsumer, NamedNode... nodes) {
+ final LanguageExpressionContentHandler handler = new LanguageExpressionContentHandler(languageName,
+ expressionConsumer);
+ for (NamedNode node : nodes) {
+ try {
+ marshaler.marshal(node, handler);
+ } catch (JAXBException e) {
+ throw new RuntimeException("Could not collect '" + languageName + "' expressions from node " + node, e);
+ }
+ }
+ }
+
+ /**
+ * Inlines all fancy expression builders so that JAXB can serialize the model properly.
+ */
+ private static class RouteDefinitionNormalizer extends Listener {
+ public void beforeMarshal(Object source) {
+ if (source instanceof ExpressionNode) {
+ ((ExpressionNode) source).preCreateProcessor();
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/LanguageExpressionContentHandler.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/LanguageExpressionContentHandler.java
new file mode 100644
index 0000000..51d6abb
--- /dev/null
+++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/LanguageExpressionContentHandler.java
@@ -0,0 +1,122 @@
+/*
+ * 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.core.deployment;
+
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class LanguageExpressionContentHandler extends DefaultHandler {
+
+ private final String languageName;
+
+ private final BiConsumer<String, Boolean> expressionConsumer;
+ private boolean inExpression = false;
+ private final StringBuilder expressionBuilder = new StringBuilder();
+ private final Deque<Map.Entry<String, Attributes>> path = new ArrayDeque<>();
+
+ public LanguageExpressionContentHandler(String languageName, BiConsumer<String, Boolean> expressionConsumer) {
+ super();
+ this.languageName = languageName;
+ this.expressionConsumer = expressionConsumer;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes atts)
+ throws SAXException {
+ if (inExpression) {
+ throw new IllegalStateException("Unexpected element '" + localName + "' under '" + languageName
+ + "'; only text content is expected");
+ }
+ if (languageName.equals(localName)) {
+ inExpression = true;
+ } else {
+ path.push(new SimpleImmutableEntry<String, Attributes>(localName, atts));
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ if (languageName.equals(localName)) {
+ final String expressionText = expressionBuilder.toString();
+ final boolean predicate = isPredicate();
+ expressionConsumer.accept(expressionText, predicate);
+ expressionBuilder.setLength(0);
+ inExpression = false;
+ } else {
+ path.pop();
+ }
+ }
+
+ private boolean isPredicate() {
+ Entry<String, Attributes> parent = path.peek();
+ if (parent != null) {
+ return hasSimplePredicateChild(parent.getKey(), attributeName -> {
+ final Attributes attribs = parent.getValue();
+ if (attribs != null) {
+ return attribs.getValue(attributeName);
+ }
+ return null;
+ });
+ }
+ return false;
+ }
+
+ /**
+ * Inspired by {@link org.apache.camel.parser.XmlRouteParser#isSimplePredicate(Node)}.
+ *
+ * @param name
+ * @param getAttributeFunction
+ * @return
+ */
+ public static boolean hasSimplePredicateChild(String name, Function<String, String> getAttributeFunction) {
+
+ if (name == null) {
+ return false;
+ }
+ if (name.equals("completionPredicate") || name.equals("completion")) {
+ return true;
+ }
+ if (name.equals("onWhen") || name.equals("when") || name.equals("handled") || name.equals("continued")) {
+ return true;
+ }
+ if (name.equals("retryWhile") || name.equals("filter") || name.equals("validate")) {
+ return true;
+ }
+ // special for loop
+ if (name.equals("loop") && "true".equalsIgnoreCase(getAttributeFunction.apply("doWhile"))) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ if (inExpression) {
+ expressionBuilder.append(ch, start, length);
+ }
+ }
+
+}
diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CSimpleExpressionSourceBuildItem.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CSimpleExpressionSourceBuildItem.java
new file mode 100644
index 0000000..1547e6f
--- /dev/null
+++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CSimpleExpressionSourceBuildItem.java
@@ -0,0 +1,58 @@
+/*
+ * 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.core.deployment.spi;
+
+import io.quarkus.builder.item.MultiBuildItem;
+
+/**
+ * A {@link MultiBuildItem} bearing info about a CSimple language expression that needs to get compiled.
+ */
+public final class CSimpleExpressionSourceBuildItem extends MultiBuildItem {
+
+ private final String sourceCode;
+ private final String classNameBase;
+ private final boolean predicate;
+
+ public CSimpleExpressionSourceBuildItem(String sourceCode, boolean predicate, String classNameBase) {
+ this.sourceCode = sourceCode;
+ this.predicate = predicate;
+ this.classNameBase = classNameBase;
+ }
+
+ /**
+ * @return the expression source code to compile
+ */
+ public String getSourceCode() {
+ return sourceCode;
+ }
+
+ /**
+ * @return a fully qualified class name that the compiler may use as a base for the name of the class into which it
+ * compiles the source code returned by {@link #getSourceCode()}
+ */
+ public String getClassNameBase() {
+ return classNameBase;
+ }
+
+ /**
+ * @return {@code true} if the expression is a predicate; {@code false} otherwise
+ */
+ public boolean isPredicate() {
+ return predicate;
+ }
+
+}
diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CompiledCSimpleExpressionBuildItem.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CompiledCSimpleExpressionBuildItem.java
new file mode 100644
index 0000000..e81139c
--- /dev/null
+++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CompiledCSimpleExpressionBuildItem.java
@@ -0,0 +1,57 @@
+/*
+ * 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.core.deployment.spi;
+
+import io.quarkus.builder.item.MultiBuildItem;
+
+/**
+ * A {@link MultiBuildItem} bearing info about a compiled CSimple language expression.
+ */
+public final class CompiledCSimpleExpressionBuildItem extends MultiBuildItem {
+
+ private final String sourceCode;
+ private final String className;
+ private final boolean predicate;
+
+ public CompiledCSimpleExpressionBuildItem(String sourceCode, boolean predicate, String className) {
+ this.sourceCode = sourceCode;
+ this.predicate = predicate;
+ this.className = className;
+ }
+
+ /**
+ * @return the source code out which the class returned by {@link #getClassName()} was compiled
+ */
+ public String getSourceCode() {
+ return sourceCode;
+ }
+
+ /**
+ * @return a fully qualified class name compiled from the source code returned by {@link #getSourceCode()}
+ */
+ public String getClassName() {
+ return className;
+ }
+
+ /**
+ * @return {@code true} if the expression is a predicate; {@code false} otherwise
+ */
+ public boolean isPredicate() {
+ return predicate;
+ }
+
+}
diff --git a/extensions-core/core/runtime/src/main/adoc/limitations.adoc b/extensions-core/core/runtime/src/main/adoc/limitations.adoc
new file mode 100644
index 0000000..9697d1c
--- /dev/null
+++ b/extensions-core/core/runtime/src/main/adoc/limitations.adoc
@@ -0,0 +1,17 @@
+=== CSimple language
+
+CSimple language is supported only in
+
+* XML DSL
+* Java DSL when implemented in a class extending `org.apache.camel.builder.RouteBuilder`
+
+The compilation of CSimple scripts happens at build time. To extract the scripts from the route definitions, these need
+to be assembled at build time. This may fail if the given route requires some data that is only available at runtime.
+You can use the `quarkus.camel.csimple.on-build-time-analysis-failure` configuration parameter to decide
+what should happen in such cases. The possible values are `warn` (default), `fail` or `ignore`.
+
+[WARNING]
+====
+CSimple language will not work on Camel Quarkus if used in a `org.apache.camel.builder.LambdaRouteBuilder`
+====
+
diff --git a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreRoutes.java b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CSimpleLanguageRecorder.java
similarity index 52%
copy from integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreRoutes.java
copy to extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CSimpleLanguageRecorder.java
index 4aa1678..d0237d7 100644
--- a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreRoutes.java
+++ b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CSimpleLanguageRecorder.java
@@ -16,17 +16,25 @@
*/
package org.apache.camel.quarkus.core;
-import org.apache.camel.builder.RouteBuilder;
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+import org.apache.camel.language.csimple.CSimpleExpression;
+import org.apache.camel.language.csimple.CSimpleLanguage;
+import org.apache.camel.language.csimple.CSimpleLanguage.Builder;
-public class CoreRoutes extends RouteBuilder {
+@Recorder
+public class CSimpleLanguageRecorder {
- @Override
- public void configure() {
- from("timer:keep-alive")
- .routeId("timer")
- .setBody().constant("I'm alive !")
- .to("log:keep-alive");
+ public RuntimeValue<CSimpleLanguage.Builder> csimpleLanguageBuilder() {
+ return new RuntimeValue<>(CSimpleLanguage.builder());
+ }
+
+ public void addExpression(RuntimeValue<Builder> builder, RuntimeValue<CSimpleExpression> expression) {
+ builder.getValue().expression(expression.getValue());
+ }
+ public RuntimeValue<?> buildCSimpleLanguage(RuntimeValue<Builder> builder) {
+ return new RuntimeValue<>(builder.getValue().build());
}
}
diff --git a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java
index 78573ef..e856007 100644
--- a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java
+++ b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java
@@ -26,6 +26,10 @@ import io.quarkus.runtime.annotations.ConfigRoot;
@ConfigRoot(name = "camel", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public class CamelConfig {
+ public enum FailureRemedy {
+ fail, warn, ignore
+ }
+
/**
* Build time configuration options for {@link CamelRuntime} bootstrap.
*/
@@ -56,6 +60,12 @@ public class CamelConfig {
@ConfigItem(name = "native")
public NativeConfig native_;
+ /**
+ * Build time configuration options for the Camel CSimple language.
+ */
+ @ConfigItem
+ public CSimpleConfig csimple;
+
@ConfigGroup
public static class BootstrapConfig {
/**
@@ -329,4 +339,12 @@ public class CamelConfig {
@ConfigItem(defaultValue = "true")
public boolean models;
}
+
+ @ConfigGroup
+ public static class CSimpleConfig {
+
+ /** What to do if it is not possible to extract CSimple expressions from a route definition at build time. */
+ @ConfigItem(defaultValue = "warn")
+ public FailureRemedy onBuildTimeAnalysisFailure;
+ }
}
diff --git a/extensions-core/main/deployment/src/main/java/org/apache/camel/quarkus/main/deployment/CSimpleXmlProcessor.java b/extensions-core/main/deployment/src/main/java/org/apache/camel/quarkus/main/deployment/CSimpleXmlProcessor.java
new file mode 100644
index 0000000..c227733
--- /dev/null
+++ b/extensions-core/main/deployment/src/main/java/org/apache/camel/quarkus/main/deployment/CSimpleXmlProcessor.java
@@ -0,0 +1,127 @@
+/*
+ * 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.main.deployment;
+
+import java.io.Closeable;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.SAXException;
+
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.engine.DefaultPackageScanResourceResolver;
+import org.apache.camel.quarkus.core.CamelConfig;
+import org.apache.camel.quarkus.core.deployment.LanguageExpressionContentHandler;
+import org.apache.camel.quarkus.core.deployment.spi.CSimpleExpressionSourceBuildItem;
+import org.apache.camel.quarkus.core.deployment.spi.CamelRoutesBuilderClassBuildItem;
+import org.apache.camel.quarkus.core.deployment.util.CamelSupport;
+import org.jboss.logging.Logger;
+
+public class CSimpleXmlProcessor {
+
+ private static final Logger LOG = Logger.getLogger(CSimpleXmlProcessor.class);
+
+ @BuildStep
+ void collectCSimpleExpresions(
+ CamelConfig config,
+ List<CamelRoutesBuilderClassBuildItem> routesBuilderClasses,
+ BuildProducer<CSimpleExpressionSourceBuildItem> csimpleExpressions)
+ throws ParserConfigurationException, SAXException, IOException {
+
+ final List<String> locations = Stream.of("camel.main.xml-routes", "camel.main.xml-rests")
+ .map(prop -> CamelSupport.getOptionalConfigValue(prop, String[].class, new String[0]))
+ .flatMap(Stream::of)
+ .collect(Collectors.toList());
+
+ try (DefaultPackageScanResourceResolver resolver = new DefaultPackageScanResourceResolver()) {
+ resolver.setCamelContext(new DefaultCamelContext());
+ final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
+ saxParserFactory.setNamespaceAware(true);
+ SAXParser saxParser = saxParserFactory.newSAXParser();
+ for (String part : locations) {
+ try {
+ try (CloseableCollection<InputStream> set = new CloseableCollection<InputStream>(
+ resolver.findResources(part))) {
+ for (InputStream is : set) {
+ LOG.debugf("Found XML routes from location: %s", part);
+ try {
+ saxParser.parse(
+ is,
+ new LanguageExpressionContentHandler(
+ "csimple",
+ (script, isPredicate) -> csimpleExpressions.produce(
+ new CSimpleExpressionSourceBuildItem(
+ script,
+ isPredicate,
+ "org.apache.camel.language.csimple.XmlRouteBuilder"))));
+ } finally {
+ if (is != null) {
+ is.close();
+ }
+ }
+ }
+ }
+ } catch (FileNotFoundException e) {
+ LOG.debugf("No XML routes found in %s. Skipping XML routes detection.", part);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not analyze CSimple expressions in " + part, e);
+ }
+ }
+ }
+ }
+
+ static class CloseableCollection<E extends Closeable> implements Closeable, Iterable<E> {
+ private final Collection<E> delegate;
+
+ public CloseableCollection(Collection<E> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void close() throws IOException {
+ List<Exception> exceptions = new ArrayList<>();
+ for (Closeable closeable : delegate) {
+ try {
+ closeable.close();
+ } catch (Exception e) {
+ exceptions.add(e);
+ }
+ }
+ if (!exceptions.isEmpty()) {
+ throw new IOException("Could not close a resource", exceptions.get(0));
+ }
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return delegate.iterator();
+ }
+ }
+}
diff --git a/integration-tests/core/pom.xml b/integration-tests/core/pom.xml
index 13c761a..9c77a65 100644
--- a/integration-tests/core/pom.xml
+++ b/integration-tests/core/pom.xml
@@ -119,6 +119,48 @@
</dependencies>
+ <build>
+ <plugins>
+<!-- <plugin>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-csimple-maven-plugin</artifactId>
+ <version>${camel.version}</version>
+ <executions>
+ <execution>
+ <id>generate</id>
+ <goals>
+ <goal>generate</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <version>3.1.0</version>
+ <executions>
+ <execution>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>add-source</goal>
+ <goal>add-resource</goal>
+ </goals>
+ <configuration>
+ <sources>
+ <source>src/generated/java</source>
+ </sources>
+ <resources>
+ <resource>
+ <directory>src/generated/resources</directory>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin> -->
+ </plugins>
+ </build>
+
<profiles>
<profile>
diff --git a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java b/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java
index 8587ef5..60e4c2f 100644
--- a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java
+++ b/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java
@@ -28,7 +28,9 @@ import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObject;
+import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
+import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@@ -39,6 +41,7 @@ import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.ExtendedCamelContext;
import org.apache.camel.NoSuchLanguageException;
+import org.apache.camel.ProducerTemplate;
import org.apache.camel.Route;
import org.apache.camel.builder.LambdaRouteBuilder;
import org.apache.camel.builder.TemplatedRouteBuilder;
@@ -59,6 +62,9 @@ public class CoreResource {
@Inject
CamelContext context;
+ @Inject
+ ProducerTemplate producerTemplate;
+
@Path("/registry/log/exchange-formatter")
@GET
@Produces(MediaType.APPLICATION_JSON)
@@ -261,4 +267,12 @@ public class CoreResource {
public boolean headersMapFactory() {
return context.adapt(ExtendedCamelContext.class).getHeadersMapFactory() instanceof DefaultHeadersMapFactory;
}
+
+ @Path("/csimple-hello")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String csimpleHello(String body) {
+ return producerTemplate.requestBody("direct:csimple-hello", body, String.class);
+ }
}
diff --git a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreRoutes.java b/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreRoutes.java
index 4aa1678..5956f38 100644
--- a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreRoutes.java
+++ b/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreRoutes.java
@@ -27,6 +27,9 @@ public class CoreRoutes extends RouteBuilder {
.setBody().constant("I'm alive !")
.to("log:keep-alive");
+ from("direct:csimple-hello")
+ .setBody().csimple("Hello ${body}");
+
}
}
diff --git a/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java b/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java
index 0ba16c9..a96f2fe 100644
--- a/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java
+++ b/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java
@@ -21,6 +21,7 @@ import java.net.HttpURLConnection;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.apache.camel.support.DefaultLRUCacheFactory;
import org.junit.jupiter.api.Test;
@@ -123,4 +124,15 @@ public class CoreTest {
void testDefaultHeadersMapFactoryConfigured() {
RestAssured.when().get("/test/headersmap-factory").then().body(is("true"));
}
+
+ @Test
+ public void csimpleHello() {
+ RestAssured.given()
+ .body("Joe")
+ .contentType(ContentType.TEXT)
+ .post("/test/csimple-hello")
+ .then()
+ .body(is("Hello Joe"));
+ }
+
}
diff --git a/integration-tests/main-xml-io/src/main/java/org/apache/camel/quarkus/main/CoreMainXmlIoResource.java b/integration-tests/main-xml-io/src/main/java/org/apache/camel/quarkus/main/CoreMainXmlIoResource.java
index d69e170..fb3ce7d 100644
--- a/integration-tests/main-xml-io/src/main/java/org/apache/camel/quarkus/main/CoreMainXmlIoResource.java
+++ b/integration-tests/main-xml-io/src/main/java/org/apache/camel/quarkus/main/CoreMainXmlIoResource.java
@@ -90,4 +90,12 @@ public class CoreMainXmlIoResource {
public String namespaceAware(String body) {
return template.requestBody("direct:namespace-aware", body, String.class);
}
+
+ @Path("/csimple-xml-dsl")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String csimpleXmlDsl(String body) {
+ return template.requestBody("direct:csimple-xml-dsl", body, String.class);
+ }
}
diff --git a/integration-tests/main-xml-io/src/main/resources/routes/my-routes.xml b/integration-tests/main-xml-io/src/main/resources/routes/my-routes.xml
index 48feb17..ae9376a 100644
--- a/integration-tests/main-xml-io/src/main/resources/routes/my-routes.xml
+++ b/integration-tests/main-xml-io/src/main/resources/routes/my-routes.xml
@@ -41,4 +41,11 @@
</setBody>
</route>
+ <route id="csimple-xml-dsl">
+ <from uri="direct:csimple-xml-dsl"/>
+ <setBody>
+ <csimple>Hi ${body}</csimple>
+ </setBody>
+ </route>
+
</routes>
\ No newline at end of file
diff --git a/integration-tests/main-xml-io/src/test/java/org/apache/camel/quarkus/main/CoreMainXmlIoTest.java b/integration-tests/main-xml-io/src/test/java/org/apache/camel/quarkus/main/CoreMainXmlIoTest.java
index 617c17b..c8c07c7 100644
--- a/integration-tests/main-xml-io/src/test/java/org/apache/camel/quarkus/main/CoreMainXmlIoTest.java
+++ b/integration-tests/main-xml-io/src/test/java/org/apache/camel/quarkus/main/CoreMainXmlIoTest.java
@@ -30,7 +30,7 @@ import org.apache.camel.xml.in.ModelParserXMLRoutesDefinitionLoader;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.hamcrest.core.Is.is;
+import static org.hamcrest.Matchers.is;
@QuarkusTest
public class CoreMainXmlIoTest {
@@ -75,4 +75,15 @@ public class CoreMainXmlIoTest {
.body(is("bar"));
}
+
+ @Test
+ public void csimpleXml() {
+ RestAssured.given()
+ .body("Joe")
+ .contentType(ContentType.TEXT)
+ .post("/test/csimple-xml-dsl")
+ .then()
+ .body(is("Hi Joe"));
+ }
+
}
diff --git a/integration-tests/main/src/test/java/org/apache/camel/quarkus/main/CoreMainTest.java b/integration-tests/main/src/test/java/org/apache/camel/quarkus/main/CoreMainTest.java
index 5ab20b9..5f6b344 100644
--- a/integration-tests/main/src/test/java/org/apache/camel/quarkus/main/CoreMainTest.java
+++ b/integration-tests/main/src/test/java/org/apache/camel/quarkus/main/CoreMainTest.java
@@ -104,7 +104,8 @@ public class CoreMainTest {
"file", "org.apache.camel.language.simple.FileLanguage",
"header", "org.apache.camel.language.header.HeaderLanguage",
"simple", "org.apache.camel.language.simple.SimpleLanguage",
- "ref", "org.apache.camel.language.ref.RefLanguage"));
+ "ref", "org.apache.camel.language.ref.RefLanguage",
+ "csimple", "org.apache.camel.language.csimple.CSimpleLanguage"));
Map<String, String> factoryFinderMap = p.getMap("factory-finder.class-map", String.class, String.class);