This is an automated email from the ASF dual-hosted git repository. ppalaga pushed a commit to branch camel-master in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 0b4216ac79fdf0e96fa8c8ee9e70314e817cb61d Author: Peter Palaga <ppal...@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 +- 18 files changed, 890 insertions(+), 9 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")); + } + }