/* * This file is part of a module with proprietary Enterprise Features. * * Licensed to Crate.io Inc. ("Crate.io") under one or more contributor * license agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * Unauthorized copying of this file, via any medium is strictly prohibited. * * To use this file, Crate.io must have given you permission to enable and * use such Enterprise Features and you must have a valid Enterprise or * Subscription Agreement with Crate.io. If you enable or use the Enterprise * Features, you represent and warrant that you have a valid Enterprise or * Subscription Agreement with Crate.io. Your use of the Enterprise Features * if governed by the terms and conditions of your Enterprise or Subscription * Agreement with Crate.io. */ package io.crate.operation.language; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import io.crate.analyze.FunctionArgumentDefinition; import io.crate.analyze.symbol.Literal; import io.crate.metadata.FunctionIdent; import io.crate.metadata.FunctionImplementation; import io.crate.metadata.Schemas; import io.crate.operation.scalar.AbstractScalarFunctionsTest; import io.crate.operation.udf.UserDefinedFunctionMetaData; import io.crate.operation.udf.UserDefinedFunctionService; import io.crate.types.ArrayType; import io.crate.types.DataType; import io.crate.types.DataTypes; import org.apache.lucene.util.BytesRef; import org.elasticsearch.cluster.service.ClusterService; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import javax.script.ScriptException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static io.crate.testing.SymbolMatchers.isLiteral; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; import static org.mockito.Mockito.mock; public class JavascriptUserDefinedFunctionTest extends AbstractScalarFunctionsTest { @Rule public final ExpectedException exception = ExpectedException.none(); private static final String JS = "javascript"; private UserDefinedFunctionService udfService; @Override @Before public void setUp() throws Exception { super.setUp(); udfService = new UserDefinedFunctionService(mock(ClusterService.class)); udfService.registerLanguage(new JavaScriptLanguage(udfService)); } private Map<FunctionIdent, FunctionImplementation> functionImplementations = new HashMap<>(); private void registerUserDefinedFunction(String name, DataType returnType, List<DataType> types, String definition) throws ScriptException { UserDefinedFunctionMetaData udfMeta = new UserDefinedFunctionMetaData( Schemas.DEFAULT_SCHEMA_NAME, name, types.stream().map(FunctionArgumentDefinition::of).collect(Collectors.toList()), returnType, JS, definition ); String validation = udfService.getLanguage(JS).validate(udfMeta); if (validation == null) { functionImplementations.put( new FunctionIdent(Schemas.DEFAULT_SCHEMA_NAME, name, types), udfService.getLanguage(JS).createFunctionImplementation(udfMeta) ); functions.registerUdfResolversForSchema(Schemas.DEFAULT_SCHEMA_NAME, functionImplementations); } else { throw new ScriptException(validation); } } @After public void after() { functionImplementations.clear(); } @Test public void testObjectReturnType() throws Exception { registerUserDefinedFunction("f", DataTypes.OBJECT, ImmutableList.of(), "function f() { return JSON.parse('{\"foo\": \"bar\"}'); }"); assertEvaluate("f()", ImmutableMap.of("foo", "bar")); } @Test public void testInvalidJavascript() throws ScriptException{ UserDefinedFunctionMetaData udfMeta = new UserDefinedFunctionMetaData( Schemas.DEFAULT_SCHEMA_NAME, "f", Collections.singletonList(FunctionArgumentDefinition.of(DataTypes.DOUBLE)), DataTypes.DOUBLE_ARRAY, JS, "function f(a) { return a[0]1*#?; }" ); String validation = udfService.getLanguage(JS).validate(udfMeta); assertThat(validation, startsWith("Invalid JavaScript in function 'doc.f(double)': <eval>:1:27")); } @Test public void testValidJavascript() throws Exception { UserDefinedFunctionMetaData udfMeta = new UserDefinedFunctionMetaData( Schemas.DEFAULT_SCHEMA_NAME, "f", Collections.singletonList(FunctionArgumentDefinition.of(DataTypes.DOUBLE_ARRAY)), DataTypes.DOUBLE, JS, "function f(a) { return a[0]; }" ); String validation = udfService.getLanguage(JS).validate(udfMeta); assertNull(validation); } @Test public void testArrayReturnType() throws Exception { registerUserDefinedFunction("f", DataTypes.DOUBLE_ARRAY, ImmutableList.of(), "function f() { return [1, 2]; }"); assertEvaluate("f()", new double[]{1.0, 2.0}); } @Test public void testTimestampReturnType() throws Exception { registerUserDefinedFunction("f", DataTypes.TIMESTAMP, ImmutableList.of(), "function f() { return \"1990-01-01T00:00:00\"; }"); assertEvaluate("f()", 631152000000L); } @Test public void testIpReturnType() throws Exception { registerUserDefinedFunction("f", DataTypes.IP, ImmutableList.of(), "function f() { return \"127.0.0.1\"; }"); assertEvaluate("f()", DataTypes.IP.value("127.0.0.1")); } @Test public void testPrimitiveReturnType() throws Exception { registerUserDefinedFunction("f", DataTypes.INTEGER, ImmutableList.of(), "function f() { return 10; }"); assertEvaluate("f()", 10); } @Test public void testObjectReturnTypeAndInputArguments() throws Exception { registerUserDefinedFunction("f", DataTypes.FLOAT, ImmutableList.of(DataTypes.DOUBLE, DataTypes.SHORT), "function f(x, y) { return x + y; }"); assertEvaluate("f(double_val, short_val)", 3.0f, Literal.of(1), Literal.of(2)); } @Test public void testPrimitiveReturnTypeAndInputArguments() throws Exception { registerUserDefinedFunction("f", DataTypes.FLOAT, ImmutableList.of(DataTypes.DOUBLE, DataTypes.SHORT), "function f(x, y) { return x + y; }"); assertEvaluate("f(double_val, short_val)", 3.0f, Literal.of(1), Literal.of(2)); } @Test public void testGeoTypeReturnTypeWithDoubleArray() throws Exception { registerUserDefinedFunction("f", DataTypes.GEO_POINT, ImmutableList.of(), "function f() { return [1, 1]; }"); assertEvaluate("f()", new double[]{1.0, 1.0}); } @Test public void testGeoTypeReturnTypeWithWKT() throws Exception { registerUserDefinedFunction("f", DataTypes.GEO_POINT, ImmutableList.of(), "function f() { return \"POINT (1.0 2.0)\"; }"); assertEvaluate("f()", new double[]{1.0, 2.0}); } @Test public void testOverloadingUserDefinedFunctions() throws Exception { registerUserDefinedFunction("f", DataTypes.LONG, ImmutableList.of(), "function f() { return 1; }"); registerUserDefinedFunction("f", DataTypes.LONG, ImmutableList.of(DataTypes.LONG), "function f(x) { return x; }"); registerUserDefinedFunction("f", DataTypes.LONG, ImmutableList.of(DataTypes.LONG, DataTypes.INTEGER), "function f(x, y) { return x + y; }"); assertEvaluate("f()", 1L); assertEvaluate("f(x)", 2L, Literal.of(2)); assertEvaluate("f(x, a)", 3L, Literal.of(2), Literal.of(1)); } @Test public void testFunctionWrongNameInFunctionBody() throws Exception { exception.expect(io.crate.exceptions.ScriptException.class); exception.expectMessage("The name of the function signature doesn't match"); registerUserDefinedFunction("test", DataTypes.LONG, ImmutableList.of(), "function f() { return 1; }"); assertEvaluate("test()", 1L); } @Test public void testNormalizeOnObjectInput() throws Exception { registerUserDefinedFunction("f", DataTypes.OBJECT, ImmutableList.of(DataTypes.OBJECT), "function f(x) { return x; }"); assertNormalize("f({})", isLiteral(new HashMap<>())); } @Test public void testNormalizeOnArrayInput() throws Exception { registerUserDefinedFunction("f", DataTypes.LONG, ImmutableList.of(DataTypes.DOUBLE_ARRAY), "function f(x) { return x[1]; }"); assertNormalize("f([1.0, 2.0])", isLiteral(2L)); } @Test public void testNormalizeOnStringInputs() throws Exception { registerUserDefinedFunction("f", DataTypes.STRING, ImmutableList.of(DataTypes.STRING), "function f(x) { return x; }"); assertNormalize("f('bar')", isLiteral("bar")); } @Test public void testAccessJavaClasses() throws Exception { exception.expect(io.crate.exceptions.ScriptException.class); exception.expectMessage(containsString("has no such function \"type\"")); registerUserDefinedFunction("f", DataTypes.LONG, ImmutableList.of(DataTypes.LONG), "function f(x) { var File = Java.type(\"java.io.File\"); return x; }"); assertEvaluate("f(x)", 1L, Literal.of(1L)); } @Test public void testEvaluateBytesRefConvertedToString() throws Exception { registerUserDefinedFunction("f", DataTypes.STRING, ImmutableList.of(DataTypes.STRING), "function f(name) { return 'foo' + name; }"); assertEvaluate("f(name)", "foobar", Literal.of("bar")); } @Test public void testJavaScriptFunctionReturnsUndefined() throws Exception { registerUserDefinedFunction("f", DataTypes.STRING, ImmutableList.of(DataTypes.STRING), "function f(name) { }"); assertEvaluate("f(name)", null, Literal.of("bar")); } @Test public void testJavaScriptFunctionReturnsNull() throws Exception { registerUserDefinedFunction("f", DataTypes.STRING, ImmutableList.of(), "function f() { return null; }"); assertEvaluate("f()", null); } @Test public void testEvaluateBytesRefInObjectIsConvertedToString() throws Exception { registerUserDefinedFunction("f", DataTypes.OBJECT, ImmutableList.of(DataTypes.OBJECT), "function f(o) { return {'key1' : o['inner']['key1'][0], 'key2': o['inner']['key2']}; }"); // the map will be modified Map<String, Object> inner = new HashMap<>(); inner.put("key1", new Object[]{new BytesRef("bar")}); inner.put("key2", new BytesRef("foo")); assertEvaluate("f(obj)", ImmutableMap.of("key1", "bar", "key2", "foo"), Literal.of(ImmutableMap.of("inner", inner))); } @Test public void testEvaluateBytesRefInArrayIsConvertedToString() throws Exception { registerUserDefinedFunction("f", DataTypes.STRING, ImmutableList.of(new ArrayType(new ArrayType(DataTypes.STRING))), "function f(arr) { return arr[0][0]; }"); assertEvaluate("f(array_string_array)", "foo", Literal.of(new Object[][]{new Object[]{new BytesRef("foo")}}, new ArrayType(new ArrayType(DataTypes.STRING)))); } }