/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.core.el.mvel;
import static java.nio.charset.StandardCharsets.UTF_16;
import static java.util.Collections.singletonList;
import static java.util.Optional.empty;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mule.runtime.api.message.Message.of;
import static org.mule.runtime.api.message.NullAttributes.NULL_ATTRIBUTES;
import static org.mule.runtime.api.metadata.DataType.OBJECT;
import static org.mule.runtime.api.metadata.DataType.STRING;
import static org.mule.runtime.api.metadata.MediaType.JSON;
import static org.mule.tck.junit4.matcher.DataTypeMatcher.like;
import org.mule.mvel2.CompileException;
import org.mule.mvel2.ParserContext;
import org.mule.mvel2.PropertyAccessException;
import org.mule.mvel2.ast.Function;
import org.mule.mvel2.optimizers.OptimizerFactory;
import org.mule.runtime.api.el.BindingContext;
import org.mule.runtime.api.el.ValidationResult;
import org.mule.runtime.api.lifecycle.InitialisationException;
import org.mule.runtime.api.metadata.AbstractDataTypeBuilderFactory;
import org.mule.runtime.api.metadata.DataType;
import org.mule.runtime.api.metadata.TypedValue;
import org.mule.runtime.core.api.Event;
import org.mule.runtime.core.api.construct.FlowConstruct;
import org.mule.runtime.core.api.el.ExpressionLanguageContext;
import org.mule.runtime.core.api.el.ExpressionLanguageExtension;
import org.mule.runtime.core.api.expression.ExpressionRuntimeException;
import org.mule.runtime.core.internal.message.InternalMessage;
import org.mule.runtime.core.api.registry.RegistrationException;
import org.mule.runtime.core.config.MuleManifest;
import org.mule.runtime.core.context.notification.DefaultFlowCallStack;
import org.mule.runtime.core.el.context.AppContext;
import org.mule.runtime.core.el.context.MessageContext;
import org.mule.runtime.core.el.function.RegexExpressionLanguageFuntion;
import org.mule.runtime.core.util.ClassUtils;
import org.mule.tck.junit4.AbstractMuleContextTestCase;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import javax.activation.DataHandler;
import javax.activation.MimeType;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class MVELExpressionLanguageTestCase extends AbstractMuleContextTestCase {
protected Variant variant;
protected MVELExpressionLanguage mvel;
private FlowConstruct flowConstruct;
final String largeExpression =
"payload = 'Tom,Fennelly,Male,4,Ireland';StringBuilder sb = new StringBuilder(); fields = payload.split(',\');"
+ "if (fields.length > 4) {" + " sb.append(' <Contact>\n');"
+ " sb.append(' <FirstName>').append(fields[0]).append('</FirstName>\n');"
+ " sb.append(' <LastName>').append(fields[1]).append('</LastName>\n');"
+ " sb.append(' <Address>').append(fields[2]).append('</Address>\n');"
+ " sb.append(' <TelNum>').append(fields[3]).append('</TelNum>\n');"
+ " sb.append(' <SIN>').append(fields[4]).append('</SIN>\n');" + " sb.append(' </Contact>\n');" + "}"
+ "sb.toString();";
public MVELExpressionLanguageTestCase(Variant variant, String mvelOptimizer) {
this.variant = variant;
OptimizerFactory.setDefaultOptimizer(mvelOptimizer);
}
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Before
public void setupMVEL() throws InitialisationException {
mvel = new MVELExpressionLanguage(muleContext);
mvel.initialise();
flowConstruct = mock(FlowConstruct.class);
when(flowConstruct.getName()).thenReturn("myFlow");
}
@Test
public void testEvaluateString() {
// Literals
assertEquals("hi", evaluate("'hi'"));
assertEquals(4, evaluate("2*2"));
assertEquals("hiho", evaluate("'hi'+'ho'"));
// Static context
assertEquals(Calendar.getInstance().getTimeZone(), evaluate("server.timeZone"));
assertEquals(MuleManifest.getProductVersion(), evaluate("mule.version"));
assertEquals(muleContext.getConfiguration().getId(), evaluate("app.name"));
}
@Test
public void testEvaluateStringMapOfStringObject() {
// Literals
assertEquals("hi", evaluate("'hi'", Collections.<String, Object>emptyMap()));
assertEquals(4, evaluate("2*2", Collections.<String, Object>emptyMap()));
// Static context
assertEquals(Calendar.getInstance().getTimeZone(), evaluate("server.timeZone", Collections.<String, Object>emptyMap()));
assertEquals(MuleManifest.getProductVersion(), evaluate("mule.version", Collections.<String, Object>emptyMap()));
assertEquals(muleContext.getConfiguration().getId(), evaluate("app.name", Collections.<String, Object>emptyMap()));
// Custom variables (via method param)
assertEquals(1, evaluate("foo", Collections.<String, Object>singletonMap("foo", 1)));
assertEquals("bar", evaluate("foo", Collections.<String, Object>singletonMap("foo", "bar")));
}
@Test
public void testEvaluateStringMuleEvent() throws Exception {
Event event = createMockEvent();
// Literals
assertEquals("hi", evaluate("'hi'", event));
assertEquals(4, evaluate("2*2", event));
// Static context
assertEquals(Calendar.getInstance().getTimeZone(), evaluate("server.timeZone", event));
assertEquals(MuleManifest.getProductVersion(), evaluate("mule.version", event));
assertEquals(muleContext.getConfiguration().getId(), evaluate("app.name", event));
// Event context
assertEquals("myFlow", evaluate("flow.name", event));
assertEquals("foo", evaluate("message.payload", event));
}
@Test
public void testEvaluateMapOfStringObject() throws Exception {
Event event = createMockEvent();
// Custom variables (via method param)
assertEquals(1, evaluate("foo", Collections.<String, Object>singletonMap("foo", 1)));
assertEquals("bar", evaluate("foo", Collections.<String, Object>singletonMap("foo", "bar")));
}
@Test
public void testEvaluateStringMuleMessage() throws Exception {
Event event = createMockEvent();
// Event context
assertEquals("foo", evaluate("message.payload", event));
}
@Test
public void testEvaluateAttributes() throws Exception {
Event event = createMockEventWithAttributes();
// Event context
assertEquals("number 1", evaluate("attributes.one", event));
assertEquals("number 2", evaluate("attributes.two", event));
}
@Test
public void testValidate() {
assertThat(validate("2*2").isSuccess(), is(true));
}
@Test
public void testValidateInvalid() {
assertThat(validate("2*'2").isSuccess(), is(false));
}
@Test
public void regexFunction() throws Exception {
final Event testEvent = eventBuilder().message(of("TESTfooTEST")).build();
assertEquals("foo", evaluate("regex('TEST(\\\\w+)TEST')", testEvent));
}
@Test
public void appTakesPrecedenceOverEverything() throws Exception {
mvel.setAliases(Collections.singletonMap("app", "'other1'"));
Event event = eventBuilder().message(of("")).addVariable("app", "otherb").build();
muleContext.getRegistry().registerObject("foo",
(ExpressionLanguageExtension) context -> context.addVariable("app", "otherc"));
mvel.initialise();
assertEquals(AppContext.class, evaluate("app", event).getClass());
}
@Test
public void messageTakesPrecedenceOverEverything() throws Exception {
mvel.setAliases(Collections.singletonMap("message", "'other1'"));
Event event = eventBuilder().message(of("")).addVariable("message", "other2").build();
muleContext.getRegistry().registerObject("foo",
(ExpressionLanguageExtension) context -> context.addVariable("message", "other3"));
mvel.initialise();
assertEquals(MessageContext.class, evaluate("message", event).getClass());
}
@Test
public void extensionTakesPrecedenceOverAutoResolved() throws Exception {
Event event = eventBuilder().message(of("")).addVariable("foo", "other").build();
muleContext.getRegistry().registerObject("key", (ExpressionLanguageExtension) context -> context.addVariable("foo", "bar"));
mvel.initialise();
assertEquals("bar", evaluate("foo", event));
}
@Test
public void aliasTakesPrecedenceOverAutoResolved() throws RegistrationException, InitialisationException {
mvel.setAliases(Collections.singletonMap("foo", "'bar'"));
muleContext.getRegistry().registerObject("key", (ExpressionLanguageExtension) context -> context.addVariable("foo", "other"));
mvel.initialise();
assertEquals("bar", evaluate("foo"));
}
@Test
public void aliasTakesPrecedenceOverExtension() throws Exception {
mvel.setAliases(Collections.singletonMap("foo", "'bar'"));
mvel.initialise();
assertEquals("bar", evaluate("foo"));
}
@Test
public void addImport() throws InitialisationException {
mvel.setImports(Collections.<String, Class<?>>singletonMap("loc", Locale.class));
mvel.initialise();
assertEquals(Locale.class, evaluate("loc"));
}
@Test
public void addAlias() throws InitialisationException {
mvel.setAliases(Collections.<String, String>singletonMap("appName", "app.name"));
mvel.initialise();
assertEquals(muleContext.getConfiguration().getId(), evaluate("appName"));
}
@Test
public void addGlobalFunction() throws InitialisationException {
mvel.addGlobalFunction("hello", new HelloWorldFunction(new ParserContext(mvel.parserConfiguration)));
mvel.initialise();
assertEquals("Hello World!", evaluate("hello()"));
}
@Test
public void defaultImports() throws InitialisationException, ClassNotFoundException, IOException {
// java.io.*
assertEquals(InputStream.class, evaluate(InputStream.class.getSimpleName()));
assertEquals(FileReader.class, evaluate(FileReader.class.getSimpleName()));
// java.lang.*
assertEquals(Object.class, evaluate(Object.class.getSimpleName()));
assertEquals(System.class, evaluate(System.class.getSimpleName()));
// java.net.*
assertEquals(URI.class, evaluate(URI.class.getSimpleName()));
assertEquals(URL.class, evaluate(URL.class.getSimpleName()));
// java.util.*
assertEquals(Collection.class, evaluate(Collection.class.getSimpleName()));
assertEquals(List.class, evaluate(List.class.getSimpleName()));
assertEquals(BigDecimal.class, evaluate(BigDecimal.class.getSimpleName()));
assertEquals(BigInteger.class, evaluate(BigInteger.class.getSimpleName()));
assertEquals(DataHandler.class, evaluate(DataHandler.class.getSimpleName()));
assertEquals(MimeType.class, evaluate(MimeType.class.getSimpleName()));
assertEquals(Pattern.class, evaluate(Pattern.class.getSimpleName()));
assertEquals(DataType.class, evaluate(DataType.class.getSimpleName()));
assertEquals(AbstractDataTypeBuilderFactory.class, evaluate(AbstractDataTypeBuilderFactory.class.getSimpleName()));
}
static class DummyExpressionLanguageExtension implements ExpressionLanguageExtension {
@Override
public void configureContext(ExpressionLanguageContext context) {
for (int i = 0; i < 20; i++) {
context.declareFunction("dummy-function-" + i, new RegexExpressionLanguageFuntion());
}
}
}
@Test
public void testConcurrentCompilation() throws Exception {
final int N = 100;
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch end = new CountDownLatch(N);
final AtomicInteger errors = new AtomicInteger(0);
for (int i = 0; i < N; i++) {
new Thread(() -> {
try {
start.await();
evaluate(largeExpression + new Random().nextInt());
} catch (Exception e) {
e.printStackTrace();
errors.incrementAndGet();
} finally {
end.countDown();
}
}, "thread-eval-" + i).start();
}
start.countDown();
end.await();
if (errors.get() > 0) {
fail();
}
}
@Test
public void testConcurrentEvaluation() throws Exception {
final int N = 100;
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch end = new CountDownLatch(N);
final AtomicInteger errors = new AtomicInteger(0);
for (int i = 0; i < N; i++) {
new Thread(() -> {
try {
start.await();
testEvaluateString();
} catch (Exception e) {
e.printStackTrace();
errors.incrementAndGet();
} finally {
end.countDown();
}
}, "thread-eval-" + i).start();
}
start.countDown();
end.await();
if (errors.get() > 0) {
fail();
}
}
@Test
public void propertyAccessException() throws InitialisationException {
try {
evaluate("doesntExist");
} catch (Exception e) {
assertEquals(ExpressionRuntimeException.class, e.getClass());
assertThat(e.getCause(), instanceOf(CompileException.class));
}
}
@Test
public void propertyAccessException2() throws InitialisationException {
try {
evaluate("app.doesntExist");
} catch (Exception e) {
assertEquals(ExpressionRuntimeException.class, e.getClass());
assertEquals(PropertyAccessException.class, e.getCause().getClass());
}
}
@Test
public void expressionExceptionHasMvelCauseMessage() throws InitialisationException {
String expressionWhichThrowsError = "doesntExist";
expectedEx.expect(ExpressionRuntimeException.class);
expectedEx.expectMessage(containsString("Error: unresolvable property or identifier: " + expressionWhichThrowsError));
expectedEx.expectMessage(containsString("evaluating expression: \"" + expressionWhichThrowsError + "\""));
evaluate(expressionWhichThrowsError);
}
@Test
public void returnsDataType() throws Exception {
DataType dataType = DataType.builder().type(String.class).mediaType(JSON).charset(UTF_16.name()).build();
Event event = createMockEvent(TEST_MESSAGE, dataType);
TypedValue typedValue = evaluateTyped("payload", event);
assertThat((String) typedValue.getValue(), equalTo(TEST_MESSAGE));
assertThat(typedValue.getDataType(), like(String.class, JSON, UTF_16));
}
protected Object evaluate(String expression) {
if (variant.equals(Variant.EXPRESSION_WITH_DELIMITER)) {
return mvel.evaluateUntyped("#[mel:" + expression + "]", null, null, null, null);
} else {
return mvel.evaluateUntyped(expression, null, null, null, null);
}
}
protected TypedValue evaluateTyped(String expression, Event event) throws Exception {
if (variant.equals(Variant.EXPRESSION_WITH_DELIMITER)) {
return mvel.evaluate("#[mel:" + expression + "]", event, Event.builder(event), flowConstruct,
BindingContext.builder().build());
} else {
return mvel.evaluate(expression, event, Event.builder(event), flowConstruct, BindingContext.builder().build());
}
}
protected Object evaluate(String expression, Map<String, Object> vars) {
if (variant.equals(Variant.EXPRESSION_WITH_DELIMITER)) {
return mvel.evaluateUntyped("#[mel:" + expression + "]", vars);
} else {
return mvel.evaluateUntyped(expression, vars);
}
}
protected Object evaluate(String expression, Event event) throws Exception {
if (variant.equals(Variant.EXPRESSION_WITH_DELIMITER)) {
return mvel.evaluateUntyped("#[mel:" + expression + "]", event, Event.builder(event), flowConstruct, null);
} else {
return mvel.evaluateUntyped(expression, event, Event.builder(event), flowConstruct, null);
}
}
protected ValidationResult validate(String expression) {
if (variant.equals(Variant.EXPRESSION_WITH_DELIMITER)) {
return mvel.validate("#[mel:" + expression + "]");
} else {
return mvel.validate(expression);
}
}
protected Event createMockEvent(DataType dataType) {
return createMockEvent("foo", STRING);
}
protected Event createMockEventWithAttributes(DataType dataType) {
HashMap<String, String> attributes = new HashMap<>();
attributes.put("one", "number 1");
attributes.put("two", "number 2");
return createMockEvent("foo", STRING, attributes, OBJECT);
}
protected Event createMockEvent(String payload, DataType dataType) {
return createMockEvent(payload, dataType, NULL_ATTRIBUTES, OBJECT);
}
protected Event createMockEvent(String payload, DataType dataType, Object attributes, DataType attributesDataType) {
Event event = mock(Event.class);
InternalMessage message = mock(InternalMessage.class);
when(message.getPayload()).thenReturn(new TypedValue<Object>(payload, dataType));
when(message.getAttributes()).thenReturn(new TypedValue<Object>(attributes, attributesDataType));
when(event.getMessage()).thenReturn(message);
when(event.getFlowCallStack()).thenReturn(new DefaultFlowCallStack());
when(event.getError()).thenReturn(empty());
return event;
}
protected Event createMockEvent() {
return createMockEvent(STRING);
}
protected Event createMockEventWithAttributes() {
return createMockEventWithAttributes(STRING);
}
public static enum Variant {
EXPRESSION_WITH_DELIMITER, EXPRESSION_STRAIGHT_UP
}
@Parameters
public static List<Object[]> parameters() {
return Arrays.asList(new Object[][] {{Variant.EXPRESSION_WITH_DELIMITER, OptimizerFactory.SAFE_REFLECTIVE},
{Variant.EXPRESSION_WITH_DELIMITER, OptimizerFactory.DYNAMIC},
{Variant.EXPRESSION_STRAIGHT_UP, OptimizerFactory.SAFE_REFLECTIVE},
{Variant.EXPRESSION_STRAIGHT_UP, OptimizerFactory.DYNAMIC}});
}
private static class HelloWorldFunction extends Function {
public HelloWorldFunction(ParserContext parserContext) {
super("hello", new char[] {}, 0, 0, 0, 0, 0, parserContext);
}
@Override
public Object call(Object ctx, Object thisValue, org.mule.mvel2.integration.VariableResolverFactory factory, Object[] parms) {
return "Hello World!";
}
}
/**
* Scans all classes accessible from the context class loader which belong to the given package and subpackages.
*
* @param packageName The base package
* @return The classes
* @throws ClassNotFoundException
* @throws IOException
*/
private static Class[] getClasses(String packageName) throws ClassNotFoundException, IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
assert classLoader != null;
String path = packageName.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
List<File> dirs = new ArrayList<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
ArrayList<Class> classes = new ArrayList<>();
for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName));
}
return classes.toArray(new Class[classes.size()]);
}
/**
* Recursive method used to find all classes in a given directory and subdirs.
*
* @param directory The base directory
* @param packageName The package name for classes found inside the base directory
* @return The classes
* @throws ClassNotFoundException
*/
private static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
List<Class> classes = new ArrayList<>();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
for (File file : files) {
if (file.getName().endsWith(".class")) {
classes.add(ClassUtils.getClass(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
}
}
return classes;
}
@Test
public void collectionAccessPayloadChangedMULE7506() throws Exception {
Event event = eventBuilder().message(of(new String[] {"1", "2"})).build();
assertEquals("1", mvel.evaluateUntyped("payload[0]", event, Event.builder(event), flowConstruct, null));
event = Event.builder(event).message(InternalMessage.builder(event.getMessage()).payload(singletonList("1")).build()).build();
assertEquals("1", mvel.evaluateUntyped("payload[0]", event, Event.builder(event), flowConstruct, null));
}
}