Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions catalog/camel-catalog/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@
<artifactId>camel-jsonpath</artifactId>
<scope>test</scope>
</dependency>
<!-- for testing groovy language -->
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-groovy</artifactId>
<scope>test</scope>
</dependency>

<!-- for testing activemq component -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,23 @@ public void testValidateJSonPathLanguage() {
assertEquals("$.store.book[?(@.price < 10)]", result.getText());
}

@Test
public void testValidateGroovyLanguage() {
LanguageValidationResult result = catalog.validateLanguageExpression(null, "groovy", "4 * 3");
assertTrue(result.isSuccess());
assertEquals("4 * 3", result.getText());

var code = """
var a = 123;
println a */ 2;
""";
result = catalog.validateLanguageExpression(null, "groovy", code);
assertFalse(result.isSuccess());
assertEquals(code, result.getText());
assertEquals(23, result.getIndex());
assertEquals("Unexpected input: '*' @ line 2, column 11.", result.getShortError());
}

@Test
public void testSpringCamelContext() {
String xml = catalog.springSchemaAsXml();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public <T> T evaluate(Exchange exchange, Class<T> type) {
}

@SuppressWarnings("unchecked")
private Script instantiateScript(Exchange exchange, Map<String, Object> globalVariables) {
protected Script instantiateScript(Exchange exchange, Map<String, Object> globalVariables) {
// Get the script from the cache, or create a new instance
GroovyLanguage language = (GroovyLanguage) exchange.getContext().resolveLanguage("groovy");
Set<GroovyShellFactory> shellFactories = exchange.getContext().getRegistry().findByType(GroovyShellFactory.class);
Expand All @@ -87,7 +87,7 @@ private Script instantiateScript(Exchange exchange, Map<String, Object> globalVa
return ObjectHelper.newInstance(scriptClass, Script.class);
}

private Binding createBinding(Exchange exchange, Map<String, Object> globalVariables) {
protected Binding createBinding(Exchange exchange, Map<String, Object> globalVariables) {
Map<String, Object> map = new HashMap<>(globalVariables);
ExchangeHelper.populateVariableMap(exchange, map, true);
map.put("log", LOG);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.apache.camel.Exchange;
import org.apache.camel.Service;
import org.apache.camel.spi.CamelEvent;
import org.apache.camel.spi.EventNotifier;
import org.apache.camel.spi.ScriptingLanguage;
import org.apache.camel.spi.annotations.Language;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.LRUCacheFactory;
import org.apache.camel.support.ObjectHelper;
import org.apache.camel.support.SimpleEventNotifierSupport;
Expand Down Expand Up @@ -156,6 +158,27 @@ public <T> T evaluate(String script, Map<String, Object> bindings, Class<T> resu
return getCamelContext().getTypeConverter().convertTo(resultType, value);
}

// use by tooling
public boolean validateExpression(String expression) throws GroovyValidationException {
final Exchange dummy = new DefaultExchange(getCamelContext());
Map<String, Object> globalVariables = new HashMap<>();

try {
GroovyExpression ge = createExpression(expression);
Script script = ge.instantiateScript(dummy, globalVariables);
script.setBinding(ge.createBinding(dummy, globalVariables));
script.run();
} catch (Exception e) {
throw new GroovyValidationException(expression, e);
}
return true;
}

// use by tooling
public boolean validatePredicate(String expression) throws GroovyValidationException {
return validateExpression(expression);
}

Class<Script> getScriptFromCache(String script) {
final GroovyClassService cached = scriptCache.get(script);
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.language.groovy;

import java.io.LineNumberReader;
import java.io.StringReader;

import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;

/**
* Exception when validating groovy scripts.
*/
public class GroovyValidationException extends Exception {

private final String script;

public GroovyValidationException(String script, Throwable cause) {
super(cause);
this.script = script;
}

public String getScript() {
return script;
}

public int getIndex() {
if (getCause() instanceof MultipleCompilationErrorsException me) {
Message gm = me.getErrorCollector().getLastError();
if (gm instanceof SyntaxErrorMessage sem) {
LineNumberReader lr = new LineNumberReader(new StringReader(script));
int pos = -1;
for (int i = 1; i < sem.getCause().getLine(); i++) {
try {
String line = lr.readLine();
pos += line.length() + System.lineSeparator().length();
} catch (Exception e) {
// ignore
}
}
pos += sem.getCause().getStartColumn();
return pos;
}
}
return -1;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@
package org.apache.camel.language.groovy;

import org.apache.camel.test.junit5.LanguageTestSupport;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

public class GroovyLanguageTest extends LanguageTestSupport {

@Test
Expand All @@ -43,6 +49,27 @@ public void testGroovyExchangeProperty() {
assertExpression("exchangeProperty.myProp2", 123);
}

@Test
public void testValidateExpression() throws Exception {
GroovyLanguage g = new GroovyLanguage();
g.setCamelContext(context);

assertTrue(g.validateExpression("2 * 3"));
assertTrue(g.validateExpression("exchange.getExchangeId()"));
assertTrue(g.validatePredicate("2 * 3 > 4"));

try {
g.validateExpression("""
var a = 123;
println a */ 2;
""");
fail("Should throw error");
} catch (GroovyValidationException e) {
assertEquals(23, e.getIndex());
assertInstanceOf(MultipleCompilationErrorsException.class, e.getCause());
}
}

@Override
protected String getLanguageName() {
return "groovy";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
package org.apache.camel.catalog.impl;

import java.io.LineNumberReader;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
Expand Down Expand Up @@ -1322,7 +1324,7 @@ private LanguageValidationResult doValidateSimple(ClassLoader classLoader, Strin
// if there are {{ }}} property placeholders then we need to resolve them to something else
// as the simple parse cannot resolve them before parsing as we dont run the actual Camel application
// with property placeholders setup so we need to dummy this by replace the {{ }} to something else
// therefore we use an more unlikely character: {{XXX}} to ~^XXX^~
// therefore we use a more unlikely character: {{XXX}} to ~^XXX^~
String resolved = simple.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~");

LanguageValidationResult answer = new LanguageValidationResult(simple);
Expand Down Expand Up @@ -1412,9 +1414,109 @@ private LanguageValidationResult doValidateSimple(ClassLoader classLoader, Strin
return answer;
}

private LanguageValidationResult doValidateGroovy(ClassLoader classLoader, String groovy, boolean predicate) {
if (classLoader == null) {
classLoader = getClass().getClassLoader();
}

// if there are {{ }}} property placeholders then we need to resolve them to something else
// as the simple parse cannot resolve them before parsing as we dont run the actual Camel application
// with property placeholders setup so we need to dummy this by replace the {{ }} to something else
// therefore we use a more unlikely character: {{XXX}} to ~^XXX^~
String resolved = groovy.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~");

LanguageValidationResult answer = new LanguageValidationResult(groovy);

Object context;
Object instance = null;
Class<?> clazz;

try {
// need a simple camel context for the groovy language parser to be able to parse
clazz = classLoader.loadClass("org.apache.camel.impl.engine.SimpleCamelContext");
context = clazz.getDeclaredConstructor(boolean.class).newInstance(false);
clazz = classLoader.loadClass("org.apache.camel.language.groovy.GroovyLanguage");
instance = clazz.getDeclaredConstructor().newInstance();
clazz = classLoader.loadClass("org.apache.camel.CamelContext");
instance.getClass().getMethod("setCamelContext", clazz).invoke(instance, context);
} catch (Exception e) {
clazz = null;
answer.setError(e.getMessage());
}

if (clazz != null) {
Throwable cause = null;
try {
if (predicate) {
instance.getClass().getMethod("validatePredicate", String.class).invoke(instance, resolved);
} else {
instance.getClass().getMethod("validateExpression", String.class).invoke(instance, resolved);
}
} catch (InvocationTargetException e) {
cause = e.getTargetException();
} catch (Exception e) {
cause = e;
}

if (cause != null) {

// reverse ~^XXX^~ back to {{XXX}}
String errMsg = cause.getMessage();
errMsg = errMsg.replaceAll("\\~\\^(.+)\\^\\~", "{{$1}}");

answer.setError(errMsg);

// is it simple parser exception then we can grab the index where the problem is
if (cause.getClass().getName().equals("org.apache.camel.language.groovy.GroovyValidationException")) {
try {
// we need to grab the index field from those simple parser exceptions
Method method = cause.getClass().getMethod("getIndex");
Object result = method.invoke(cause);
if (result != null) {
int index = (int) result;
answer.setIndex(index);
}
} catch (Exception i) {
// ignore
}
}

// we need to grab the short message field from this simple syntax exception
if (answer.getShortError() == null) {
// fallback and try to make existing message short instead
String msg = answer.getError();
// grab everything before " @ " which would be regarded as the short message
LineNumberReader lnr = new LineNumberReader(new StringReader(msg));
try {
String line = lnr.readLine();
do {
if (line.contains(" @ ")) {
// skip leading Scrip_xxxx.groovy: N:
if (line.startsWith("Script_") && StringHelper.countChar(line, ':') > 2) {
line = StringHelper.after(line, ":", line);
line = StringHelper.after(line, ":", line);
line = line.trim();
}
answer.setShortError(line);
break;
}
line = lnr.readLine();
} while (line != null);
} catch (Exception e) {
// ignore
}
}
}
}

return answer;
}

public LanguageValidationResult validateLanguagePredicate(ClassLoader classLoader, String language, String text) {
if ("simple".equals(language)) {
return doValidateSimple(classLoader, text, true);
} else if ("groovy".equals(language)) {
return doValidateGroovy(classLoader, text, true);
} else {
return doValidateLanguage(classLoader, language, text, true);
}
Expand All @@ -1423,6 +1525,8 @@ public LanguageValidationResult validateLanguagePredicate(ClassLoader classLoade
public LanguageValidationResult validateLanguageExpression(ClassLoader classLoader, String language, String text) {
if ("simple".equals(language)) {
return doValidateSimple(classLoader, text, false);
} else if ("groovy".equals(language)) {
return doValidateGroovy(classLoader, text, false);
} else {
return doValidateLanguage(classLoader, language, text, false);
}
Expand Down