Browse Source

migrate branch for lang-javascript

Simon Willnauer 10 years ago
parent
commit
64fdb0eeb0
16 changed files with 2433 additions and 0 deletions
  1. 177 0
      plugins/lang-javascript/README.md
  2. 43 0
      plugins/lang-javascript/pom.xml
  3. 26 0
      plugins/lang-javascript/src/main/assemblies/plugin.xml
  4. 44 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/plugin/javascript/JavaScriptPlugin.java
  5. 311 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/JavaScriptScriptEngineService.java
  6. 149 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/NativeList.java
  7. 223 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/NativeMap.java
  8. 183 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptValueConverter.java
  9. 188 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptableLinkedHashMap.java
  10. 32 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptableMap.java
  11. 342 0
      plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptableWrappedMap.java
  12. 2 0
      plugins/lang-javascript/src/main/resources/es-plugin.properties
  13. 174 0
      plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/JavaScriptScriptEngineTests.java
  14. 169 0
      plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/JavaScriptScriptMultiThreadedTest.java
  15. 299 0
      plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/JavaScriptScriptSearchTests.java
  16. 71 0
      plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/SimpleBench.java

+ 177 - 0
plugins/lang-javascript/README.md

@@ -0,0 +1,177 @@
+JavaScript lang Plugin for Elasticsearch
+==================================
+
+The JavaScript language plugin allows to have `javascript` (or `js`) as the language of scripts to execute.
+
+In order to install the plugin, simply run: 
+
+```sh
+bin/plugin install elasticsearch/elasticsearch-lang-javascript/2.5.0
+```
+
+You need to install a version matching your Elasticsearch version:
+
+| elasticsearch |   JavaScript Plugin   |   Docs     |  
+|---------------|-----------------------|------------|
+| master        |  Build from source    | See below  |
+| es-1.x        |  Build from source    | [2.6.0-SNAPSHOT](https://github.com/elasticsearch/elasticsearch-transport-thrift/tree/es-1.x/#version-260-snapshot-for-elasticsearch-1x)  |
+|    es-1.5              |     2.5.0         | [2.5.0](https://github.com/elastic/elasticsearch-lang-javascript/tree/v2.5.0/#version-250-for-elasticsearch-15)                  |
+|    es-1.4              |     2.4.1         | [2.4.1](https://github.com/elasticsearch/elasticsearch-lang-javascript/tree/v2.4.1/#version-241-for-elasticsearch-14)                  |
+|    es-1.3              |     2.3.1         | [2.3.1](https://github.com/elasticsearch/elasticsearch-lang-javascript/tree/v2.3.1/#version-231-for-elasticsearch-13)                  |
+| es-1.2        |  2.2.0                | [2.2.0](https://github.com/elasticsearch/elasticsearch-lang-javascript/tree/v2.2.0/#javascript-lang-plugin-for-elasticsearch)  |
+| es-1.1        |  2.1.0                | [2.1.0](https://github.com/elasticsearch/elasticsearch-lang-javascript/tree/v2.1.0/#javascript-lang-plugin-for-elasticsearch)  |
+| es-1.0        |  2.0.0                | [2.0.0](https://github.com/elasticsearch/elasticsearch-lang-javascript/tree/v2.0.0/#javascript-lang-plugin-for-elasticsearch)  |
+| es-0.90       |  1.4.0                | [1.4.0](https://github.com/elasticsearch/elasticsearch-lang-javascript/tree/v1.4.0/#javascript-lang-plugin-for-elasticsearch)  |
+
+To build a `SNAPSHOT` version, you need to build it with Maven:
+
+```bash
+mvn clean install
+plugin --install lang-javascript \
+       --url file:target/releases/elasticsearch-lang-javascript-X.X.X-SNAPSHOT.zip
+```
+
+
+Using javascript with function_score
+------------------------------------
+
+Let's say you want to use `function_score` API using `javascript`. Here is
+a way of doing it:
+
+```sh
+curl -XDELETE "http://localhost:9200/test"
+
+curl -XPUT "http://localhost:9200/test/doc/1" -d '{
+  "num": 1.0
+}'
+
+curl -XPUT "http://localhost:9200/test/doc/2?refresh" -d '{
+  "num": 2.0
+}'
+
+curl -XGET "http://localhost:9200/test/_search?pretty" -d '
+{
+  "query": {
+    "function_score": {
+      "script_score": {
+        "script": "doc[\"num\"].value",
+        "lang": "javascript"
+      }
+    }
+  }
+}'
+```
+
+gives
+
+```javascript
+{
+   // ...
+   "hits": {
+      "total": 2,
+      "max_score": 4,
+      "hits": [
+         {
+            // ...
+            "_score": 4
+         },
+         {
+            // ...
+            "_score": 1
+         }
+      ]
+   }
+}
+```
+
+Using javascript with script_fields
+-----------------------------------
+
+```sh
+curl -XDELETE "http://localhost:9200/test"
+
+curl -XPUT "http://localhost:9200/test/doc/1?refresh" -d'
+{
+  "obj1": {
+   "test": "something"
+  },
+  "obj2": {
+    "arr2": [ "arr_value1", "arr_value2" ]
+  }
+}'
+
+curl -XGET "http://localhost:9200/test/_search" -d'
+{
+  "script_fields": {
+    "s_obj1": {
+      "script": "_source.obj1", "lang": "js"
+    },
+    "s_obj1_test": {
+      "script": "_source.obj1.test", "lang": "js"
+    },
+    "s_obj2": {
+      "script": "_source.obj2", "lang": "js"
+    },
+    "s_obj2_arr2": {
+      "script": "_source.obj2.arr2", "lang": "js"
+    }
+  }
+}'
+```
+
+gives
+
+```javascript
+{
+  // ...
+  "hits": [
+     {
+        // ...
+        "fields": {
+           "s_obj2_arr2": [
+              [
+                 "arr_value1",
+                 "arr_value2"
+              ]
+           ],
+           "s_obj1_test": [
+              "something"
+           ],
+           "s_obj2": [
+              {
+                 "arr2": [
+                    "arr_value1",
+                    "arr_value2"
+                 ]
+              }
+           ],
+           "s_obj1": [
+              {
+                 "test": "something"
+              }
+           ]
+        }
+     }
+  ]
+}
+```
+
+
+License
+-------
+
+    This software is licensed under the Apache 2 license, quoted below.
+
+    Copyright 2009-2014 Elasticsearch <http://www.elasticsearch.org>
+
+    Licensed 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.

+ 43 - 0
plugins/lang-javascript/pom.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.elasticsearch.plugin</groupId>
+    <artifactId>elasticsearch-lang-javascript</artifactId>
+
+    <packaging>jar</packaging>
+    <name>Elasticsearch JavaScript language plugin</name>
+    <description>The JavaScript language plugin allows to have javascript as the language of scripts to execute.</description>
+
+    <parent>
+        <groupId>org.elasticsearch</groupId>
+        <artifactId>elasticsearch-plugin</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+    </parent>
+
+    <properties>
+        <!-- You can add any specific project property here -->
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.mozilla</groupId>
+            <artifactId>rhino</artifactId>
+            <version>1.7R4</version>
+            <exclusions>
+            </exclusions>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 26 - 0
plugins/lang-javascript/src/main/assemblies/plugin.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<assembly>
+    <id>plugin</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <dependencySets>
+        <dependencySet>
+            <outputDirectory>/</outputDirectory>
+            <useProjectArtifact>true</useProjectArtifact>
+            <useTransitiveFiltering>true</useTransitiveFiltering>
+            <excludes>
+                <exclude>org.elasticsearch:elasticsearch</exclude>
+            </excludes>
+        </dependencySet>
+        <dependencySet>
+            <outputDirectory>/</outputDirectory>
+            <useProjectArtifact>true</useProjectArtifact>
+            <useTransitiveFiltering>true</useTransitiveFiltering>
+            <includes>
+                <include>org.mozilla:rhino</include>
+            </includes>
+        </dependencySet>
+    </dependencySets>
+</assembly>

+ 44 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/plugin/javascript/JavaScriptPlugin.java

@@ -0,0 +1,44 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.plugin.javascript;
+
+import org.elasticsearch.plugins.AbstractPlugin;
+import org.elasticsearch.script.ScriptModule;
+import org.elasticsearch.script.javascript.JavaScriptScriptEngineService;
+
+/**
+ *
+ */
+public class JavaScriptPlugin extends AbstractPlugin {
+
+    @Override
+    public String name() {
+        return "lang-javascript";
+    }
+
+    @Override
+    public String description() {
+        return "JavaScript plugin allowing to add javascript scripting support";
+    }
+
+    public void onModule(ScriptModule module) {
+        module.addScriptEngine(JavaScriptScriptEngineService.class);
+    }
+}

+ 311 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/JavaScriptScriptEngineService.java

@@ -0,0 +1,311 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript;
+
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.Scorer;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.component.AbstractComponent;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.script.*;
+import org.elasticsearch.script.javascript.support.NativeList;
+import org.elasticsearch.script.javascript.support.NativeMap;
+import org.elasticsearch.script.javascript.support.ScriptValueConverter;
+import org.elasticsearch.search.lookup.LeafSearchLookup;
+import org.elasticsearch.search.lookup.SearchLookup;
+import org.mozilla.javascript.*;
+import org.mozilla.javascript.Script;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ *
+ */
+public class JavaScriptScriptEngineService extends AbstractComponent implements ScriptEngineService {
+
+    private final AtomicLong counter = new AtomicLong();
+
+    private static WrapFactory wrapFactory = new CustomWrapFactory();
+
+    private final int optimizationLevel;
+
+    private Scriptable globalScope;
+
+    @Inject
+    public JavaScriptScriptEngineService(Settings settings) {
+        super(settings);
+
+        this.optimizationLevel = settings.getAsInt("script.javascript.optimization_level", 1);
+
+        Context ctx = Context.enter();
+        try {
+            ctx.setWrapFactory(wrapFactory);
+            globalScope = ctx.initStandardObjects(null, true);
+        } finally {
+            Context.exit();
+        }
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public void scriptRemoved(@Nullable CompiledScript compiledScript) {
+        // Nothing to do here
+    }
+
+    @Override
+    public String[] types() {
+        return new String[]{"js", "javascript"};
+    }
+
+    @Override
+    public String[] extensions() {
+        return new String[]{"js"};
+    }
+
+    @Override
+    public boolean sandboxed() {
+        return false;
+    }
+
+    @Override
+    public Object compile(String script) {
+        Context ctx = Context.enter();
+        try {
+            ctx.setWrapFactory(wrapFactory);
+            ctx.setOptimizationLevel(optimizationLevel);
+            return ctx.compileString(script, generateScriptName(), 1, null);
+        } finally {
+            Context.exit();
+        }
+    }
+
+    @Override
+    public ExecutableScript executable(Object compiledScript, Map<String, Object> vars) {
+        Context ctx = Context.enter();
+        try {
+            ctx.setWrapFactory(wrapFactory);
+
+            Scriptable scope = ctx.newObject(globalScope);
+            scope.setPrototype(globalScope);
+            scope.setParentScope(null);
+            for (Map.Entry<String, Object> entry : vars.entrySet()) {
+                ScriptableObject.putProperty(scope, entry.getKey(), entry.getValue());
+            }
+
+            return new JavaScriptExecutableScript((Script) compiledScript, scope);
+        } finally {
+            Context.exit();
+        }
+    }
+
+    @Override
+    public SearchScript search(final Object compiledScript, final SearchLookup lookup, @Nullable final Map<String, Object> vars) {
+        Context ctx = Context.enter();
+        try {
+            ctx.setWrapFactory(wrapFactory);
+
+            final Scriptable scope = ctx.newObject(globalScope);
+            scope.setPrototype(globalScope);
+            scope.setParentScope(null);
+
+            return new SearchScript() {
+
+              @Override
+              public LeafSearchScript getLeafSearchScript(LeafReaderContext context) throws IOException {
+                final LeafSearchLookup leafLookup = lookup.getLeafSearchLookup(context);
+                for (Map.Entry<String, Object> entry : leafLookup.asMap().entrySet()) {
+                    ScriptableObject.putProperty(scope, entry.getKey(), entry.getValue());
+                }
+
+                if (vars != null) {
+                    for (Map.Entry<String, Object> entry : vars.entrySet()) {
+                        ScriptableObject.putProperty(scope, entry.getKey(), entry.getValue());
+                    }
+                }
+
+                return new JavaScriptSearchScript((Script) compiledScript, scope, leafLookup);
+              }
+            };
+        } finally {
+            Context.exit();
+        }
+    }
+
+    @Override
+    public Object execute(Object compiledScript, Map<String, Object> vars) {
+        Context ctx = Context.enter();
+        ctx.setWrapFactory(wrapFactory);
+        try {
+            Script script = (Script) compiledScript;
+            Scriptable scope = ctx.newObject(globalScope);
+            scope.setPrototype(globalScope);
+            scope.setParentScope(null);
+
+            for (Map.Entry<String, Object> entry : vars.entrySet()) {
+                ScriptableObject.putProperty(scope, entry.getKey(), entry.getValue());
+            }
+            Object ret = script.exec(ctx, scope);
+            return ScriptValueConverter.unwrapValue(ret);
+        } finally {
+            Context.exit();
+        }
+    }
+
+    @Override
+    public Object unwrap(Object value) {
+        return ScriptValueConverter.unwrapValue(value);
+    }
+
+    private String generateScriptName() {
+        return "Script" + counter.incrementAndGet() + ".js";
+    }
+
+    public static class JavaScriptExecutableScript implements ExecutableScript {
+
+        private final Script script;
+
+        private final Scriptable scope;
+
+        public JavaScriptExecutableScript(Script script, Scriptable scope) {
+            this.script = script;
+            this.scope = scope;
+        }
+
+        @Override
+        public Object run() {
+            Context ctx = Context.enter();
+            try {
+                ctx.setWrapFactory(wrapFactory);
+                return ScriptValueConverter.unwrapValue(script.exec(ctx, scope));
+            } finally {
+                Context.exit();
+            }
+        }
+
+        @Override
+        public void setNextVar(String name, Object value) {
+            ScriptableObject.putProperty(scope, name, value);
+        }
+
+        @Override
+        public Object unwrap(Object value) {
+            return ScriptValueConverter.unwrapValue(value);
+        }
+    }
+
+    public static class JavaScriptSearchScript implements LeafSearchScript {
+
+        private final Script script;
+
+        private final Scriptable scope;
+
+        private final LeafSearchLookup lookup;
+
+        public JavaScriptSearchScript(Script script, Scriptable scope, LeafSearchLookup lookup) {
+            this.script = script;
+            this.scope = scope;
+            this.lookup = lookup;
+        }
+
+        @Override
+        public void setScorer(Scorer scorer) {
+            Context ctx = Context.enter();
+            try {
+              ScriptableObject.putProperty(scope, "_score", wrapFactory.wrapAsJavaObject(ctx, scope, new ScoreAccessor(scorer), ScoreAccessor.class));
+            } finally {
+              Context.exit();
+            }
+        }
+
+        @Override
+        public void setDocument(int doc) {
+            lookup.setDocument(doc);
+        }
+
+        @Override
+        public void setNextVar(String name, Object value) {
+            ScriptableObject.putProperty(scope, name, value);
+        }
+
+        @Override
+        public void setSource(Map<String, Object> source) {
+            lookup.source().setSource(source);
+        }
+
+        @Override
+        public Object run() {
+            Context ctx = Context.enter();
+            try {
+                ctx.setWrapFactory(wrapFactory);
+                return ScriptValueConverter.unwrapValue(script.exec(ctx, scope));
+            } finally {
+                Context.exit();
+            }
+        }
+
+        @Override
+        public float runAsFloat() {
+            return ((Number) run()).floatValue();
+        }
+
+        @Override
+        public long runAsLong() {
+            return ((Number) run()).longValue();
+        }
+
+        @Override
+        public double runAsDouble() {
+            return ((Number) run()).doubleValue();
+        }
+
+        @Override
+        public Object unwrap(Object value) {
+            return ScriptValueConverter.unwrapValue(value);
+        }
+    }
+
+    /**
+     * Wrap Factory for Rhino Script Engine
+     */
+    public static class CustomWrapFactory extends WrapFactory {
+
+        public CustomWrapFactory() {
+            setJavaPrimitiveWrap(false); // RingoJS does that..., claims its annoying...
+        }
+
+        public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType) {
+            if (javaObject instanceof Map) {
+                return NativeMap.wrap(scope, (Map) javaObject);
+            }
+            if (javaObject instanceof List) {
+                return NativeList.wrap(scope, (List) javaObject, staticType);
+            }
+            return super.wrapAsJavaObject(cx, scope, javaObject, staticType);
+        }
+    }
+}

+ 149 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/NativeList.java

@@ -0,0 +1,149 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript.support;
+
+import org.mozilla.javascript.NativeJavaObject;
+import org.mozilla.javascript.Scriptable;
+import org.mozilla.javascript.Undefined;
+import org.mozilla.javascript.Wrapper;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ *
+ */
+public class NativeList extends NativeJavaObject implements Scriptable, Wrapper {
+    private static final long serialVersionUID = 3664761893203964569L;
+    private static final String LENGTH_PROPERTY = "length";
+
+    private final List<Object> list;
+
+
+    public static NativeList wrap(Scriptable scope, List<Object> list, Class<?> staticType) {
+        return new NativeList(scope, list, staticType);
+    }
+
+    private NativeList(Scriptable scope, List<Object> list, Class<?> staticType) {
+        super(scope, list, staticType);
+        this.list = list;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Wrapper#unwrap()
+     */
+
+    public Object unwrap() {
+        return list;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getClassName()
+     */
+
+    public String getClassName() {
+        return "NativeList";
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#get(java.lang.String, org.mozilla.javascript.Scriptable)
+     */
+
+    public Object get(String name, Scriptable start) {
+        if (LENGTH_PROPERTY.equals(name)) {
+            return list.size();
+        } else {
+            return super.get(name, start);
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#get(int, org.mozilla.javascript.Scriptable)
+     */
+
+    public Object get(int index, Scriptable start) {
+        if (has(index, start) == false) {
+            return Undefined.instance;
+        }
+        return list.get(index);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#has(java.lang.String, org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean has(String name, Scriptable start) {
+        return super.has(name, start) || LENGTH_PROPERTY.equals(name);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#has(int, org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean has(int index, Scriptable start) {
+        return index >= 0 && index < list.size();
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#put(int, org.mozilla.javascript.Scriptable, java.lang.Object)
+     */
+
+    public void put(int index, Scriptable start, Object value) {
+        if (index == list.size()) {
+            list.add(value);
+        } else {
+            list.set(index, value);
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#delete(int)
+     */
+
+    public void delete(int index) {
+        list.remove(index);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getIds()
+     */
+
+    public Object[] getIds() {
+        final Object[] javaObjectIds = super.getIds();
+        final int size = list.size();
+        final Object[] ids = Arrays.copyOf(javaObjectIds, javaObjectIds.length + size);
+        for (int i = 0; i < size; ++i) {
+            ids[javaObjectIds.length + i] = i;
+        }
+        return ids;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#hasInstance(org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean hasInstance(Scriptable value) {
+        if (!(value instanceof Wrapper))
+            return false;
+        Object instance = ((Wrapper) value).unwrap();
+        return List.class.isInstance(instance);
+    }
+
+}

+ 223 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/NativeMap.java

@@ -0,0 +1,223 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript.support;
+
+import org.mozilla.javascript.Scriptable;
+import org.mozilla.javascript.Wrapper;
+
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Wrapper for exposing maps in Rhino scripts.
+ *
+ *
+ */
+public class NativeMap implements Scriptable, Wrapper {
+    private static final long serialVersionUID = 3664761893203964569L;
+
+    private Map<Object, Object> map;
+    private Scriptable parentScope;
+    private Scriptable prototype;
+
+
+    /**
+     * Construct
+     *
+     * @param scope
+     * @param map
+     * @return native map
+     */
+    public static NativeMap wrap(Scriptable scope, Map<Object, Object> map) {
+        return new NativeMap(scope, map);
+    }
+
+    /**
+     * Construct
+     *
+     * @param scope
+     * @param map
+     */
+    private NativeMap(Scriptable scope, Map<Object, Object> map) {
+        this.parentScope = scope;
+        this.map = map;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Wrapper#unwrap()
+     */
+
+    public Object unwrap() {
+        return map;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getClassName()
+     */
+
+    public String getClassName() {
+        return "NativeMap";
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#get(java.lang.String, org.mozilla.javascript.Scriptable)
+     */
+
+    public Object get(String name, Scriptable start) {
+        // get the property from the underlying QName map
+        if ("length".equals(name)) {
+            return map.size();
+        } else {
+            return map.get(name);
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#get(int, org.mozilla.javascript.Scriptable)
+     */
+
+    public Object get(int index, Scriptable start) {
+        Object value = null;
+        int i = 0;
+        Iterator itrValues = map.values().iterator();
+        while (i++ <= index && itrValues.hasNext()) {
+            value = itrValues.next();
+        }
+        return value;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#has(java.lang.String, org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean has(String name, Scriptable start) {
+        // locate the property in the underlying map
+        return map.containsKey(name);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#has(int, org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean has(int index, Scriptable start) {
+        return (index >= 0 && map.values().size() > index);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#put(java.lang.String, org.mozilla.javascript.Scriptable, java.lang.Object)
+     */
+
+    @SuppressWarnings("unchecked")
+    public void put(String name, Scriptable start, Object value) {
+        map.put(name, value);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#put(int, org.mozilla.javascript.Scriptable, java.lang.Object)
+     */
+
+    public void put(int index, Scriptable start, Object value) {
+        // TODO: implement?
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#delete(java.lang.String)
+     */
+
+    public void delete(String name) {
+        map.remove(name);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#delete(int)
+     */
+
+    public void delete(int index) {
+        int i = 0;
+        Iterator itrKeys = map.keySet().iterator();
+        while (i <= index && itrKeys.hasNext()) {
+            Object key = itrKeys.next();
+            if (i == index) {
+                map.remove(key);
+                break;
+            }
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getPrototype()
+     */
+
+    public Scriptable getPrototype() {
+        return this.prototype;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#setPrototype(org.mozilla.javascript.Scriptable)
+     */
+
+    public void setPrototype(Scriptable prototype) {
+        this.prototype = prototype;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getParentScope()
+     */
+
+    public Scriptable getParentScope() {
+        return this.parentScope;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#setParentScope(org.mozilla.javascript.Scriptable)
+     */
+
+    public void setParentScope(Scriptable parent) {
+        this.parentScope = parent;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getIds()
+     */
+
+    public Object[] getIds() {
+        return map.keySet().toArray();
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getDefaultValue(java.lang.Class)
+     */
+
+    public Object getDefaultValue(Class hint) {
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#hasInstance(org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean hasInstance(Scriptable value) {
+        if (!(value instanceof Wrapper))
+            return false;
+        Object instance = ((Wrapper) value).unwrap();
+        return Map.class.isInstance(instance);
+    }
+
+}

+ 183 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptValueConverter.java

@@ -0,0 +1,183 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript.support;
+
+import org.mozilla.javascript.*;
+
+import java.util.*;
+
+/**
+ * Value Converter to marshal objects between Java and Javascript.
+ *
+ *
+ */
+public final class ScriptValueConverter {
+    private static final String TYPE_DATE = "Date";
+
+
+    /**
+     * Private constructor - methods are static
+     */
+    private ScriptValueConverter() {
+    }
+
+    /**
+     * Convert an object from a script wrapper value to a serializable value valid outside
+     * of the Rhino script processor context.
+     * <p/>
+     * This includes converting JavaScript Array objects to Lists of valid objects.
+     *
+     * @param value Value to convert from script wrapper object to external object value.
+     * @return unwrapped and converted value.
+     */
+    public static Object unwrapValue(Object value) {
+        if (value == null) {
+            return null;
+        } else if (value instanceof Wrapper) {
+            // unwrap a Java object from a JavaScript wrapper
+            // recursively call this method to convert the unwrapped value
+            value = unwrapValue(((Wrapper) value).unwrap());
+        } else if (value instanceof IdScriptableObject) {
+            // check for special case Native object wrappers
+            String className = ((IdScriptableObject) value).getClassName();
+            // check for special case of the String object
+            if ("String".equals(className)) {
+                value = Context.jsToJava(value, String.class);
+            }
+            // check for special case of a Date object
+            else if ("Date".equals(className)) {
+                value = Context.jsToJava(value, Date.class);
+            } else {
+                // a scriptable object will probably indicate a multi-value property set
+                // set using a JavaScript associative Array object
+                Scriptable values = (Scriptable) value;
+                Object[] propIds = values.getIds();
+
+                // is it a JavaScript associative Array object using Integer indexes?
+                if (values instanceof NativeArray && isArray(propIds)) {
+                    // convert JavaScript array of values to a List of Serializable objects
+                    List<Object> propValues = new ArrayList<Object>(propIds.length);
+                    for (int i = 0; i < propIds.length; i++) {
+                        // work on each key in turn
+                        Integer propId = (Integer) propIds[i];
+
+                        // we are only interested in keys that indicate a list of values
+                        if (propId instanceof Integer) {
+                            // get the value out for the specified key
+                            Object val = values.get(propId, values);
+                            // recursively call this method to convert the value
+                            propValues.add(unwrapValue(val));
+                        }
+                    }
+
+                    value = propValues;
+                } else {
+                    // any other JavaScript object that supports properties - convert to a Map of objects
+                    Map<String, Object> propValues = new HashMap<String, Object>(propIds.length);
+                    for (int i = 0; i < propIds.length; i++) {
+                        // work on each key in turn
+                        Object propId = propIds[i];
+
+                        // we are only interested in keys that indicate a list of values
+                        if (propId instanceof String) {
+                            // get the value out for the specified key
+                            Object val = values.get((String) propId, values);
+                            // recursively call this method to convert the value
+                            propValues.put((String) propId, unwrapValue(val));
+                        }
+                    }
+                    value = propValues;
+                }
+            }
+        } else if (value instanceof Object[]) {
+            // convert back a list Object Java values
+            Object[] array = (Object[]) value;
+            ArrayList<Object> list = new ArrayList<Object>(array.length);
+            for (int i = 0; i < array.length; i++) {
+                list.add(unwrapValue(array[i]));
+            }
+            value = list;
+        } else if (value instanceof Map) {
+            // ensure each value in the Map is unwrapped (which may have been an unwrapped NativeMap!)
+            Map<Object, Object> map = (Map<Object, Object>) value;
+            Map<Object, Object> copyMap = new HashMap<Object, Object>(map.size());
+            for (Object key : map.keySet()) {
+                copyMap.put(key, unwrapValue(map.get(key)));
+            }
+            value = copyMap;
+        }
+        return value;
+    }
+
+    /**
+     * Convert an object from any repository serialized value to a valid script object.
+     * This includes converting Collection multi-value properties into JavaScript Array objects.
+     *
+     * @param scope Scripting scope
+     * @param value Property value
+     * @return Value safe for scripting usage
+     */
+    public static Object wrapValue(Scriptable scope, Object value) {
+        // perform conversions from Java objects to JavaScript scriptable instances
+        if (value == null) {
+            return null;
+        } else if (value instanceof Date) {
+            // convert Date to JavaScript native Date object
+            // call the "Date" constructor on the root scope object - passing in the millisecond
+            // value from the Java date - this will construct a JavaScript Date with the same value
+            Date date = (Date) value;
+            value = ScriptRuntime.newObject(
+                    Context.getCurrentContext(), scope, TYPE_DATE, new Object[]{date.getTime()});
+        } else if (value instanceof Collection) {
+            // recursively convert each value in the collection
+            Collection<Object> collection = (Collection<Object>) value;
+            Object[] array = new Object[collection.size()];
+            int index = 0;
+            for (Object obj : collection) {
+                array[index++] = wrapValue(scope, obj);
+            }
+            // convert array to a native JavaScript Array
+            value = Context.getCurrentContext().newArray(scope, array);
+        } else if (value instanceof Map) {
+            value = NativeMap.wrap(scope, (Map) value);
+        }
+
+        // simple numbers, strings and booleans are wrapped automatically by Rhino
+
+        return value;
+    }
+
+    /**
+     * Look at the id's of a native array and try to determine whether it's actually an Array or a Hashmap
+     *
+     * @param ids id's of the native array
+     * @return boolean  true if it's an array, false otherwise (ie it's a map)
+     */
+    private static boolean isArray(final Object[] ids) {
+        boolean result = true;
+        for (int i = 0; i < ids.length; i++) {
+            if (ids[i] instanceof Integer == false) {
+                result = false;
+                break;
+            }
+        }
+        return result;
+    }
+}

+ 188 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptableLinkedHashMap.java

@@ -0,0 +1,188 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript.support;
+
+import org.mozilla.javascript.Scriptable;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Implementation of a Scriptable Map. This is the best choice for maps that want to represent
+ * JavaScript associative arrays - allowing access via key and integer index. It maintains and
+ * respects insertion order of the elements and allows either string or integer keys.
+ *
+ *
+ */
+public class ScriptableLinkedHashMap<K, V> extends LinkedHashMap<K, V> implements ScriptableMap<K, V> {
+    private static final long serialVersionUID = 3774167893214964123L;
+
+    private Scriptable parentScope;
+    private Scriptable prototype;
+
+
+    public ScriptableLinkedHashMap() {
+    }
+
+    public ScriptableLinkedHashMap(int initialCapacity) {
+        super(initialCapacity);
+    }
+
+    public ScriptableLinkedHashMap(Map<K, V> source) {
+        super(source);
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#getClassName()
+     */
+    public String getClassName() {
+        return "ScriptableMap";
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#get(java.lang.String, org.mozilla.javascript.Scriptable)
+     */
+    public Object get(String name, Scriptable start) {
+        // get the property from the underlying QName map
+        if ("length".equals(name)) {
+            return this.size();
+        } else {
+            return get(name);
+        }
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#get(int, org.mozilla.javascript.Scriptable)
+     */
+    public Object get(int index, Scriptable start) {
+        Object value = null;
+        int i = 0;
+        Iterator itrValues = this.values().iterator();
+        while (i++ <= index && itrValues.hasNext()) {
+            value = itrValues.next();
+        }
+        return value;
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#has(java.lang.String, org.mozilla.javascript.Scriptable)
+     */
+    public boolean has(String name, Scriptable start) {
+        // locate the property in the underlying map
+        return containsKey(name);
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#has(int, org.mozilla.javascript.Scriptable)
+     */
+    public boolean has(int index, Scriptable start) {
+        return (index >= 0 && this.values().size() > index);
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#put(java.lang.String, org.mozilla.javascript.Scriptable, java.lang.Object)
+     */
+    @SuppressWarnings("unchecked")
+    public void put(String name, Scriptable start, Object value) {
+        // add the property to the underlying QName map
+        put((K) name, (V) value);
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#put(int, org.mozilla.javascript.Scriptable, java.lang.Object)
+     */
+    public void put(int index, Scriptable start, Object value) {
+        // TODO: implement?
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#delete(java.lang.String)
+     */
+    public void delete(String name) {
+        // remove the property from the underlying QName map
+        remove(name);
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#delete(int)
+     */
+    public void delete(int index) {
+        int i = 0;
+        Iterator itrKeys = this.keySet().iterator();
+        while (i <= index && itrKeys.hasNext()) {
+            Object key = itrKeys.next();
+            if (i == index) {
+                remove(key);
+                break;
+            }
+        }
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#getPrototype()
+     */
+    public Scriptable getPrototype() {
+        return this.prototype;
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#setPrototype(org.mozilla.javascript.Scriptable)
+     */
+    public void setPrototype(Scriptable prototype) {
+        this.prototype = prototype;
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#getParentScope()
+     */
+    public Scriptable getParentScope() {
+        return this.parentScope;
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#setParentScope(org.mozilla.javascript.Scriptable)
+     */
+    public void setParentScope(Scriptable parent) {
+        this.parentScope = parent;
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#getIds()
+     */
+    public Object[] getIds() {
+        return keySet().toArray();
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#getDefaultValue(java.lang.Class)
+     */
+    public Object getDefaultValue(Class hint) {
+        return null;
+    }
+
+    /**
+     * @see org.mozilla.javascript.Scriptable#hasInstance(org.mozilla.javascript.Scriptable)
+     */
+    public boolean hasInstance(Scriptable instance) {
+        return instance instanceof ScriptableLinkedHashMap;
+    }
+}
+

+ 32 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptableMap.java

@@ -0,0 +1,32 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript.support;
+
+import org.mozilla.javascript.Scriptable;
+
+import java.util.Map;
+
+/**
+ * Contract to be implemented by classes providing Map like collections to JavaScript.
+ *
+ *
+ */
+public interface ScriptableMap<K, V> extends Scriptable, Map<K, V> {
+}

+ 342 - 0
plugins/lang-javascript/src/main/java/org/elasticsearch/script/javascript/support/ScriptableWrappedMap.java

@@ -0,0 +1,342 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript.support;
+
+import org.mozilla.javascript.Scriptable;
+import org.mozilla.javascript.Wrapper;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Implementation of a Scriptable Map. This is the best choice where you want values to be
+ * persisted directly to an underlying map supplied on construction. The class automatically
+ * wraps/unwraps JS objects as they enter/leave the underlying map via the Scriptable interface
+ * methods - objects are untouched if accessed via the usual Map interface methods.
+ * <p/>
+ * <p>Access should be by string key only - not integer index - unless you are sure the wrapped
+ * map will maintain insertion order of the elements.
+ *
+ *
+ */
+public class ScriptableWrappedMap implements ScriptableMap, Wrapper {
+    private Map map;
+    private Scriptable parentScope;
+    private Scriptable prototype;
+
+
+    /**
+     * Construction
+     *
+     * @param scope
+     * @param map
+     * @return scriptable wrapped map
+     */
+    public static ScriptableWrappedMap wrap(Scriptable scope, Map<Object, Object> map) {
+        return new ScriptableWrappedMap(scope, map);
+    }
+
+    /**
+     * Construct
+     *
+     * @param map
+     */
+    public ScriptableWrappedMap(Map map) {
+        this.map = map;
+    }
+
+    /**
+     * Construct
+     *
+     * @param scope
+     * @param map
+     */
+    public ScriptableWrappedMap(Scriptable scope, Map map) {
+        this.parentScope = scope;
+        this.map = map;
+    }
+
+    /* (non-Javadoc)
+    * @see org.mozilla.javascript.Wrapper#unwrap()
+    */
+
+    public Object unwrap() {
+        return map;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getClassName()
+     */
+
+    public String getClassName() {
+        return "ScriptableWrappedMap";
+    }
+
+    /* (non-Javadoc)
+    * @see org.mozilla.javascript.Scriptable#get(java.lang.String, org.mozilla.javascript.Scriptable)
+    */
+
+    public Object get(String name, Scriptable start) {
+        // get the property from the underlying QName map
+        if ("length".equals(name)) {
+            return map.size();
+        } else {
+            return ScriptValueConverter.wrapValue(this.parentScope != null ? this.parentScope : start, map.get(name));
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#get(int, org.mozilla.javascript.Scriptable)
+     */
+
+    public Object get(int index, Scriptable start) {
+        Object value = null;
+        int i = 0;
+        Iterator itrValues = map.values().iterator();
+        while (i++ <= index && itrValues.hasNext()) {
+            value = itrValues.next();
+        }
+        return ScriptValueConverter.wrapValue(this.parentScope != null ? this.parentScope : start, value);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#has(java.lang.String, org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean has(String name, Scriptable start) {
+        // locate the property in the underlying map
+        return map.containsKey(name);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#has(int, org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean has(int index, Scriptable start) {
+        return (index >= 0 && map.values().size() > index);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#put(java.lang.String, org.mozilla.javascript.Scriptable, java.lang.Object)
+     */
+
+    @SuppressWarnings("unchecked")
+    public void put(String name, Scriptable start, Object value) {
+        map.put(name, ScriptValueConverter.unwrapValue(value));
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#put(int, org.mozilla.javascript.Scriptable, java.lang.Object)
+     */
+
+    public void put(int index, Scriptable start, Object value) {
+        // TODO: implement?
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#delete(java.lang.String)
+     */
+
+    public void delete(String name) {
+        map.remove(name);
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#delete(int)
+     */
+
+    public void delete(int index) {
+        int i = 0;
+        Iterator itrKeys = map.keySet().iterator();
+        while (i <= index && itrKeys.hasNext()) {
+            Object key = itrKeys.next();
+            if (i == index) {
+                map.remove(key);
+                break;
+            }
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getPrototype()
+     */
+
+    public Scriptable getPrototype() {
+        return this.prototype;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#setPrototype(org.mozilla.javascript.Scriptable)
+     */
+
+    public void setPrototype(Scriptable prototype) {
+        this.prototype = prototype;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getParentScope()
+     */
+
+    public Scriptable getParentScope() {
+        return this.parentScope;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#setParentScope(org.mozilla.javascript.Scriptable)
+     */
+
+    public void setParentScope(Scriptable parent) {
+        this.parentScope = parent;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getIds()
+     */
+
+    public Object[] getIds() {
+        return map.keySet().toArray();
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#getDefaultValue(java.lang.Class)
+     */
+
+    public Object getDefaultValue(Class hint) {
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.mozilla.javascript.Scriptable#hasInstance(org.mozilla.javascript.Scriptable)
+     */
+
+    public boolean hasInstance(Scriptable value) {
+        if (!(value instanceof Wrapper))
+            return false;
+        Object instance = ((Wrapper) value).unwrap();
+        return Map.class.isInstance(instance);
+    }
+
+    /* (non-Javadoc)
+    * @see java.util.Map#clear()
+    */
+
+    public void clear() {
+        this.map.clear();
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#containsKey(java.lang.Object)
+     */
+
+    public boolean containsKey(Object key) {
+        return this.map.containsKey(key);
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#containsValue(java.lang.Object)
+     */
+
+    public boolean containsValue(Object value) {
+        return this.map.containsValue(value);
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#entrySet()
+     */
+
+    public Set entrySet() {
+        return this.map.entrySet();
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#get(java.lang.Object)
+     */
+
+    public Object get(Object key) {
+        return this.map.get(key);
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#isEmpty()
+     */
+
+    public boolean isEmpty() {
+        return (this.map.size() == 0);
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#keySet()
+     */
+
+    public Set keySet() {
+        return this.map.keySet();
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#put(java.lang.Object, java.lang.Object)
+     */
+
+    public Object put(Object key, Object value) {
+        return this.map.put(key, value);
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#putAll(java.util.Map)
+     */
+
+    public void putAll(Map t) {
+        this.map.putAll(t);
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#remove(java.lang.Object)
+     */
+
+    public Object remove(Object key) {
+        return this.map.remove(key);
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#size()
+     */
+
+    public int size() {
+        return this.map.size();
+    }
+
+    /* (non-Javadoc)
+     * @see java.util.Map#values()
+     */
+
+    public Collection values() {
+        return this.map.values();
+    }
+
+    /* (non-Javadoc)
+     * @see java.lang.Object#toString()
+     */
+
+    @Override
+    public String toString() {
+        return (this.map != null ? this.map.toString() : super.toString());
+    }
+}

+ 2 - 0
plugins/lang-javascript/src/main/resources/es-plugin.properties

@@ -0,0 +1,2 @@
+plugin=org.elasticsearch.plugin.javascript.JavaScriptPlugin
+version=${project.version}

+ 174 - 0
plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/JavaScriptScriptEngineTests.java

@@ -0,0 +1,174 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript;
+
+import org.elasticsearch.common.collect.MapBuilder;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.script.ExecutableScript;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+
+/**
+ *
+ */
+public class JavaScriptScriptEngineTests extends ElasticsearchTestCase {
+
+    private JavaScriptScriptEngineService se;
+
+    @Before
+    public void setup() {
+        se = new JavaScriptScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
+    }
+
+    @After
+    public void close() {
+        se.close();
+    }
+
+    @Test
+    public void testSimpleEquation() {
+        Map<String, Object> vars = new HashMap<String, Object>();
+        Object o = se.execute(se.compile("1 + 2"), vars);
+        assertThat(((Number) o).intValue(), equalTo(3));
+    }
+
+    @Test
+    public void testMapAccess() {
+        Map<String, Object> vars = new HashMap<String, Object>();
+
+        Map<String, Object> obj2 = MapBuilder.<String, Object>newMapBuilder().put("prop2", "value2").map();
+        Map<String, Object> obj1 = MapBuilder.<String, Object>newMapBuilder().put("prop1", "value1").put("obj2", obj2).put("l", Arrays.asList("2", "1")).map();
+        vars.put("obj1", obj1);
+        Object o = se.execute(se.compile("obj1"), vars);
+        assertThat(o, instanceOf(Map.class));
+        obj1 = (Map<String, Object>) o;
+        assertThat((String) obj1.get("prop1"), equalTo("value1"));
+        assertThat((String) ((Map<String, Object>) obj1.get("obj2")).get("prop2"), equalTo("value2"));
+
+        o = se.execute(se.compile("obj1.l[0]"), vars);
+        assertThat(((String) o), equalTo("2"));
+    }
+
+    @Test
+    public void testJavaScriptObjectToMap() {
+        Map<String, Object> vars = new HashMap<String, Object>();
+        Object o = se.execute(se.compile("var obj1 = {}; obj1.prop1 = 'value1'; obj1.obj2 = {}; obj1.obj2.prop2 = 'value2'; obj1"), vars);
+        Map obj1 = (Map) o;
+        assertThat((String) obj1.get("prop1"), equalTo("value1"));
+        assertThat((String) ((Map<String, Object>) obj1.get("obj2")).get("prop2"), equalTo("value2"));
+    }
+
+    @Test
+    public void testJavaScriptObjectMapInter() {
+        Map<String, Object> vars = new HashMap<String, Object>();
+        Map<String, Object> ctx = new HashMap<String, Object>();
+        Map<String, Object> obj1 = new HashMap<String, Object>();
+        obj1.put("prop1", "value1");
+        ctx.put("obj1", obj1);
+        vars.put("ctx", ctx);
+
+        se.execute(se.compile("ctx.obj2 = {}; ctx.obj2.prop2 = 'value2'; ctx.obj1.prop1 = 'uvalue1'"), vars);
+        ctx = (Map<String, Object>) se.unwrap(vars.get("ctx"));
+        assertThat(ctx.containsKey("obj1"), equalTo(true));
+        assertThat((String) ((Map<String, Object>) ctx.get("obj1")).get("prop1"), equalTo("uvalue1"));
+        assertThat(ctx.containsKey("obj2"), equalTo(true));
+        assertThat((String) ((Map<String, Object>) ctx.get("obj2")).get("prop2"), equalTo("value2"));
+    }
+
+    @Test
+    public void testJavaScriptInnerArrayCreation() {
+        Map<String, Object> ctx = new HashMap<String, Object>();
+        Map<String, Object> doc = new HashMap<String, Object>();
+        ctx.put("doc", doc);
+
+        Object complied = se.compile("ctx.doc.field1 = ['value1', 'value2']");
+        ExecutableScript script = se.executable(complied, new HashMap<String, Object>());
+        script.setNextVar("ctx", ctx);
+        script.run();
+
+        Map<String, Object> unwrap = (Map<String, Object>) script.unwrap(ctx);
+
+        assertThat(((Map) unwrap.get("doc")).get("field1"), instanceOf(List.class));
+    }
+
+    @Test
+    public void testAccessListInScript() {
+        Map<String, Object> vars = new HashMap<String, Object>();
+        Map<String, Object> obj2 = MapBuilder.<String, Object>newMapBuilder().put("prop2", "value2").map();
+        Map<String, Object> obj1 = MapBuilder.<String, Object>newMapBuilder().put("prop1", "value1").put("obj2", obj2).map();
+        vars.put("l", Arrays.asList("1", "2", "3", obj1));
+
+        Object o = se.execute(se.compile("l.length"), vars);
+        assertThat(((Number) o).intValue(), equalTo(4));
+
+        o = se.execute(se.compile("l[0]"), vars);
+        assertThat(((String) o), equalTo("1"));
+
+        o = se.execute(se.compile("l[3]"), vars);
+        obj1 = (Map<String, Object>) o;
+        assertThat((String) obj1.get("prop1"), equalTo("value1"));
+        assertThat((String) ((Map<String, Object>) obj1.get("obj2")).get("prop2"), equalTo("value2"));
+
+        o = se.execute(se.compile("l[3].prop1"), vars);
+        assertThat(((String) o), equalTo("value1"));
+    }
+
+    @Test
+    public void testChangingVarsCrossExecution1() {
+        Map<String, Object> vars = new HashMap<String, Object>();
+        Map<String, Object> ctx = new HashMap<String, Object>();
+        vars.put("ctx", ctx);
+        Object compiledScript = se.compile("ctx.value");
+
+        ExecutableScript script = se.executable(compiledScript, vars);
+        ctx.put("value", 1);
+        Object o = script.run();
+        assertThat(((Number) o).intValue(), equalTo(1));
+
+        ctx.put("value", 2);
+        o = script.run();
+        assertThat(((Number) o).intValue(), equalTo(2));
+    }
+
+    @Test
+    public void testChangingVarsCrossExecution2() {
+        Map<String, Object> vars = new HashMap<String, Object>();
+        Object compiledScript = se.compile("value");
+
+        ExecutableScript script = se.executable(compiledScript, vars);
+        script.setNextVar("value", 1);
+        Object o = script.run();
+        assertThat(((Number) o).intValue(), equalTo(1));
+
+        script.setNextVar("value", 2);
+        o = script.run();
+        assertThat(((Number) o).intValue(), equalTo(2));
+    }
+}

+ 169 - 0
plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/JavaScriptScriptMultiThreadedTest.java

@@ -0,0 +1,169 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript;
+
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.script.ExecutableScript;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ *
+ */
+public class JavaScriptScriptMultiThreadedTest extends ElasticsearchTestCase {
+
+    @Test
+    public void testExecutableNoRuntimeParams() throws Exception {
+        final JavaScriptScriptEngineService se = new JavaScriptScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
+        final Object compiled = se.compile("x + y");
+        final AtomicBoolean failed = new AtomicBoolean();
+
+        Thread[] threads = new Thread[50];
+        final CountDownLatch latch = new CountDownLatch(threads.length);
+        final CyclicBarrier barrier = new CyclicBarrier(threads.length + 1);
+        for (int i = 0; i < threads.length; i++) {
+            threads[i] = new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        barrier.await();
+                        long x = ThreadLocalRandom.current().nextInt();
+                        long y = ThreadLocalRandom.current().nextInt();
+                        long addition = x + y;
+                        Map<String, Object> vars = new HashMap<String, Object>();
+                        vars.put("x", x);
+                        vars.put("y", y);
+                        ExecutableScript script = se.executable(compiled, vars);
+                        for (int i = 0; i < 100000; i++) {
+                            long result = ((Number) script.run()).longValue();
+                            assertThat(result, equalTo(addition));
+                        }
+                    } catch (Throwable t) {
+                        failed.set(true);
+                        logger.error("failed", t);
+                    } finally {
+                        latch.countDown();
+                    }
+                }
+            });
+        }
+        for (int i = 0; i < threads.length; i++) {
+            threads[i].start();
+        }
+        barrier.await();
+        latch.await();
+        assertThat(failed.get(), equalTo(false));
+    }
+
+
+    @Test
+    public void testExecutableWithRuntimeParams() throws Exception {
+        final JavaScriptScriptEngineService se = new JavaScriptScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
+        final Object compiled = se.compile("x + y");
+        final AtomicBoolean failed = new AtomicBoolean();
+
+        Thread[] threads = new Thread[50];
+        final CountDownLatch latch = new CountDownLatch(threads.length);
+        final CyclicBarrier barrier = new CyclicBarrier(threads.length + 1);
+        for (int i = 0; i < threads.length; i++) {
+            threads[i] = new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        barrier.await();
+                        long x = ThreadLocalRandom.current().nextInt();
+                        Map<String, Object> vars = new HashMap<String, Object>();
+                        vars.put("x", x);
+                        ExecutableScript script = se.executable(compiled, vars);
+                        for (int i = 0; i < 100000; i++) {
+                            long y = ThreadLocalRandom.current().nextInt();
+                            long addition = x + y;
+                            script.setNextVar("y", y);
+                            long result = ((Number) script.run()).longValue();
+                            assertThat(result, equalTo(addition));
+                        }
+                    } catch (Throwable t) {
+                        failed.set(true);
+                        logger.error("failed", t);
+                    } finally {
+                        latch.countDown();
+                    }
+                }
+            });
+        }
+        for (int i = 0; i < threads.length; i++) {
+            threads[i].start();
+        }
+        barrier.await();
+        latch.await();
+        assertThat(failed.get(), equalTo(false));
+    }
+
+    @Test
+    public void testExecute() throws Exception {
+        final JavaScriptScriptEngineService se = new JavaScriptScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
+        final Object compiled = se.compile("x + y");
+        final AtomicBoolean failed = new AtomicBoolean();
+
+        Thread[] threads = new Thread[50];
+        final CountDownLatch latch = new CountDownLatch(threads.length);
+        final CyclicBarrier barrier = new CyclicBarrier(threads.length + 1);
+        for (int i = 0; i < threads.length; i++) {
+            threads[i] = new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        barrier.await();
+                        Map<String, Object> runtimeVars = new HashMap<String, Object>();
+                        for (int i = 0; i < 100000; i++) {
+                            long x = ThreadLocalRandom.current().nextInt();
+                            long y = ThreadLocalRandom.current().nextInt();
+                            long addition = x + y;
+                            runtimeVars.put("x", x);
+                            runtimeVars.put("y", y);
+                            long result = ((Number) se.execute(compiled, runtimeVars)).longValue();
+                            assertThat(result, equalTo(addition));
+                        }
+                    } catch (Throwable t) {
+                        failed.set(true);
+                        logger.error("failed", t);
+                    } finally {
+                        latch.countDown();
+                    }
+                }
+            });
+        }
+        for (int i = 0; i < threads.length; i++) {
+            threads[i].start();
+        }
+        barrier.await();
+        latch.await();
+        assertThat(failed.get(), equalTo(false));
+    }
+}

+ 299 - 0
plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/JavaScriptScriptSearchTests.java

@@ -0,0 +1,299 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript;
+
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.search.SearchType;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
+import org.elasticsearch.plugins.PluginsService;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.elasticsearch.client.Requests.searchRequest;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.index.query.QueryBuilders.*;
+import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.scriptFunction;
+import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
+import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+/**
+ *
+ */
+@ElasticsearchIntegrationTest.ClusterScope(scope = ElasticsearchIntegrationTest.Scope.SUITE)
+public class JavaScriptScriptSearchTests extends ElasticsearchIntegrationTest {
+
+    @Override
+    protected Settings nodeSettings(int nodeOrdinal) {
+        return Settings.builder()
+                .put(super.nodeSettings(nodeOrdinal))
+                .put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, true)
+                .build();
+    }
+
+    @Test
+    public void testJavaScriptFilter() throws Exception {
+        createIndex("test");
+        index("test", "type1", "1", jsonBuilder().startObject().field("test", "value beck").field("num1", 1.0f).endObject());
+        flush();
+        index("test", "type1", "2", jsonBuilder().startObject().field("test", "value beck").field("num1", 2.0f).endObject());
+        flush();
+        index("test", "type1", "3", jsonBuilder().startObject().field("test", "value beck").field("num1", 3.0f).endObject());
+        refresh();
+
+        logger.info(" --> running doc['num1'].value > 1");
+        SearchResponse response = client().prepareSearch()
+                .setQuery(filteredQuery(matchAllQuery(), scriptQuery("doc['num1'].value > 1").lang("js")))
+                .addSort("num1", SortOrder.ASC)
+                .addScriptField("sNum1", "js", "doc['num1'].value", null)
+                .execute().actionGet();
+
+        assertThat(response.getHits().totalHits(), equalTo(2l));
+        assertThat(response.getHits().getAt(0).id(), equalTo("2"));
+        assertThat((Double) response.getHits().getAt(0).fields().get("sNum1").values().get(0), equalTo(2.0));
+        assertThat(response.getHits().getAt(1).id(), equalTo("3"));
+        assertThat((Double) response.getHits().getAt(1).fields().get("sNum1").values().get(0), equalTo(3.0));
+
+        logger.info(" --> running doc['num1'].value > param1");
+        response = client().prepareSearch()
+                .setQuery(filteredQuery(matchAllQuery(), scriptQuery("doc['num1'].value > param1").lang("js").addParam("param1", 2)))
+                .addSort("num1", SortOrder.ASC)
+                .addScriptField("sNum1", "js", "doc['num1'].value", null)
+                .execute().actionGet();
+
+        assertThat(response.getHits().totalHits(), equalTo(1l));
+        assertThat(response.getHits().getAt(0).id(), equalTo("3"));
+        assertThat((Double) response.getHits().getAt(0).fields().get("sNum1").values().get(0), equalTo(3.0));
+
+        logger.info(" --> running doc['num1'].value > param1");
+        response = client().prepareSearch()
+                .setQuery(filteredQuery(matchAllQuery(), scriptQuery("doc['num1'].value > param1").lang("js").addParam("param1", -1)))
+                .addSort("num1", SortOrder.ASC)
+                .addScriptField("sNum1", "js", "doc['num1'].value", null)
+                .execute().actionGet();
+
+        assertThat(response.getHits().totalHits(), equalTo(3l));
+        assertThat(response.getHits().getAt(0).id(), equalTo("1"));
+        assertThat((Double) response.getHits().getAt(0).fields().get("sNum1").values().get(0), equalTo(1.0));
+        assertThat(response.getHits().getAt(1).id(), equalTo("2"));
+        assertThat((Double) response.getHits().getAt(1).fields().get("sNum1").values().get(0), equalTo(2.0));
+        assertThat(response.getHits().getAt(2).id(), equalTo("3"));
+        assertThat((Double) response.getHits().getAt(2).fields().get("sNum1").values().get(0), equalTo(3.0));
+    }
+
+    @Test
+    public void testScriptFieldUsingSource() throws Exception {
+        createIndex("test");
+        index("test", "type1", "1",
+                jsonBuilder().startObject()
+                        .startObject("obj1").field("test", "something").endObject()
+                        .startObject("obj2").startArray("arr2").value("arr_value1").value("arr_value2").endArray().endObject()
+                        .endObject());
+        refresh();
+
+        SearchResponse response = client().prepareSearch()
+                .setQuery(matchAllQuery())
+                .addScriptField("s_obj1", "js", "_source.obj1", null)
+                .addScriptField("s_obj1_test", "js", "_source.obj1.test", null)
+                .addScriptField("s_obj2", "js", "_source.obj2", null)
+                .addScriptField("s_obj2_arr2", "js", "_source.obj2.arr2", null)
+                .execute().actionGet();
+
+        Map<String, Object> sObj1 = (Map<String, Object>) response.getHits().getAt(0).field("s_obj1").value();
+        assertThat(sObj1.get("test").toString(), equalTo("something"));
+        assertThat(response.getHits().getAt(0).field("s_obj1_test").value().toString(), equalTo("something"));
+
+        Map<String, Object> sObj2 = (Map<String, Object>) response.getHits().getAt(0).field("s_obj2").value();
+        List sObj2Arr2 = (List) sObj2.get("arr2");
+        assertThat(sObj2Arr2.size(), equalTo(2));
+        assertThat(sObj2Arr2.get(0).toString(), equalTo("arr_value1"));
+        assertThat(sObj2Arr2.get(1).toString(), equalTo("arr_value2"));
+
+        sObj2Arr2 = (List) response.getHits().getAt(0).field("s_obj2_arr2").values();
+        assertThat(sObj2Arr2.size(), equalTo(2));
+        assertThat(sObj2Arr2.get(0).toString(), equalTo("arr_value1"));
+        assertThat(sObj2Arr2.get(1).toString(), equalTo("arr_value2"));
+    }
+
+    @Test
+    public void testCustomScriptBoost() throws Exception {
+        createIndex("test");
+        index("test", "type1", "1", jsonBuilder().startObject().field("test", "value beck").field("num1", 1.0f).endObject());
+        index("test", "type1", "2", jsonBuilder().startObject().field("test", "value beck").field("num1", 2.0f).endObject());
+        refresh();
+
+        logger.info("--- QUERY_THEN_FETCH");
+
+        logger.info(" --> running doc['num1'].value");
+        SearchResponse response = client().search(searchRequest()
+                .searchType(SearchType.QUERY_THEN_FETCH)
+                .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value"))
+                        .add(ScoreFunctionBuilders.scriptFunction("doc['num1'].value").lang("js"))))
+        ).actionGet();
+
+        assertThat("Failures " + Arrays.toString(response.getShardFailures()), response.getShardFailures().length, equalTo(0));
+
+        assertThat(response.getHits().totalHits(), equalTo(2l));
+        logger.info(" --> Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation());
+        logger.info(" --> Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation());
+        assertThat(response.getHits().getAt(0).id(), equalTo("2"));
+        assertThat(response.getHits().getAt(1).id(), equalTo("1"));
+
+        logger.info(" --> running -doc['num1'].value");
+        response = client().search(searchRequest()
+                .searchType(SearchType.QUERY_THEN_FETCH)
+                .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value"))
+                        .add(ScoreFunctionBuilders.scriptFunction("-doc['num1'].value").lang("js"))))
+        ).actionGet();
+
+        assertThat("Failures " + Arrays.toString(response.getShardFailures()), response.getShardFailures().length, equalTo(0));
+
+        assertThat(response.getHits().totalHits(), equalTo(2l));
+        logger.info(" --> Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation());
+        logger.info(" --> Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation());
+        assertThat(response.getHits().getAt(0).id(), equalTo("1"));
+        assertThat(response.getHits().getAt(1).id(), equalTo("2"));
+
+
+        logger.info(" --> running pow(doc['num1'].value, 2)");
+        response = client().search(searchRequest()
+                .searchType(SearchType.QUERY_THEN_FETCH)
+                .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value"))
+                        .add(ScoreFunctionBuilders.scriptFunction("Math.pow(doc['num1'].value, 2)").lang("js"))))
+        ).actionGet();
+
+        assertThat("Failures " + Arrays.toString(response.getShardFailures()), response.getShardFailures().length, equalTo(0));
+
+        assertThat(response.getHits().totalHits(), equalTo(2l));
+        logger.info(" --> Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation());
+        logger.info(" --> Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation());
+        assertThat(response.getHits().getAt(0).id(), equalTo("2"));
+        assertThat(response.getHits().getAt(1).id(), equalTo("1"));
+
+        logger.info(" --> running max(doc['num1'].value, 1)");
+        response = client().search(searchRequest()
+                .searchType(SearchType.QUERY_THEN_FETCH)
+                .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value"))
+                        .add(ScoreFunctionBuilders.scriptFunction("Math.max(doc['num1'].value, 1)").lang("js"))))
+        ).actionGet();
+
+        assertThat("Failures " + Arrays.toString(response.getShardFailures()), response.getShardFailures().length, equalTo(0));
+
+        assertThat(response.getHits().totalHits(), equalTo(2l));
+        logger.info(" --> Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation());
+        logger.info(" --> Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation());
+        assertThat(response.getHits().getAt(0).id(), equalTo("2"));
+        assertThat(response.getHits().getAt(1).id(), equalTo("1"));
+
+        logger.info(" --> running doc['num1'].value * _score");
+        response = client().search(searchRequest()
+                .searchType(SearchType.QUERY_THEN_FETCH)
+                .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value"))
+                        .add(ScoreFunctionBuilders.scriptFunction("doc['num1'].value * _score").lang("js"))))
+        ).actionGet();
+
+        assertThat("Failures " + Arrays.toString(response.getShardFailures()), response.getShardFailures().length, equalTo(0));
+
+        assertThat(response.getHits().totalHits(), equalTo(2l));
+        logger.info(" --> Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation());
+        logger.info(" --> Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation());
+        assertThat(response.getHits().getAt(0).id(), equalTo("2"));
+        assertThat(response.getHits().getAt(1).id(), equalTo("1"));
+
+        logger.info(" --> running param1 * param2 * _score");
+        response = client().search(searchRequest()
+                .searchType(SearchType.QUERY_THEN_FETCH)
+                .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value"))
+                        .add(ScoreFunctionBuilders.scriptFunction("param1 * param2 * _score").param("param1", 2).param("param2", 2).lang("js"))))
+        ).actionGet();
+
+        assertThat("Failures " + Arrays.toString(response.getShardFailures()), response.getShardFailures().length, equalTo(0));
+
+        assertThat(response.getHits().totalHits(), equalTo(2l));
+        logger.info(" --> Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation());
+        logger.info(" --> Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation());
+    }
+
+    @Test
+    public void testScriptScoresNested() throws IOException {
+        createIndex("index");
+        ensureYellow();
+        index("index", "testtype", "1", jsonBuilder().startObject().field("dummy_field", 1).endObject());
+        refresh();
+        SearchResponse response = client().search(
+                searchRequest().source(
+                        searchSource().query(
+                                functionScoreQuery(
+                                        functionScoreQuery(
+                                                functionScoreQuery().add(scriptFunction("1").lang("js")))
+                                                .add(scriptFunction("_score.doubleValue()").lang("js")))
+                                        .add(scriptFunction("_score.doubleValue()").lang("js")
+                                        )
+                        )
+                )
+        ).actionGet();
+        assertSearchResponse(response);
+        assertThat(response.getHits().getAt(0).score(), equalTo(1.0f));
+    }
+
+    @Test
+    public void testScriptScoresWithAgg() throws IOException {
+        createIndex("index");
+        ensureYellow();
+        index("index", "testtype", "1", jsonBuilder().startObject().field("dummy_field", 1).endObject());
+        refresh();
+        SearchResponse response = client().search(
+                searchRequest().source(
+                        searchSource().query(
+                                functionScoreQuery()
+                                        .add(scriptFunction("_score.doubleValue()").lang("js")
+                                        )
+                        ).aggregation(terms("score_agg").script("_score.doubleValue()").lang("js"))
+                )
+        ).actionGet();
+        assertSearchResponse(response);
+        assertThat(response.getHits().getAt(0).score(), equalTo(1.0f));
+        assertThat(((Terms) response.getAggregations().asMap().get("score_agg")).getBuckets().get(0).getKeyAsNumber().floatValue(), is(1f));
+        assertThat(((Terms) response.getAggregations().asMap().get("score_agg")).getBuckets().get(0).getDocCount(), is(1l));
+    }
+
+    @Test
+    public void testUseListLengthInScripts() throws Exception {
+        createIndex("index");
+        index("index", "testtype", "1", jsonBuilder().startObject().field("f", 42).endObject());
+        ensureSearchable("index");
+        refresh();
+        SearchResponse response = client().prepareSearch().addScriptField("foobar", "js", "doc['f'].values.length", null).get();
+        assertSearchResponse(response);
+        assertHitCount(response, 1);
+        assertThat((Integer) response.getHits().getAt(0).getFields().get("foobar").value(), equalTo(1));
+    }
+}

+ 71 - 0
plugins/lang-javascript/src/test/java/org/elasticsearch/script/javascript/SimpleBench.java

@@ -0,0 +1,71 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch 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.elasticsearch.script.javascript;
+
+import org.elasticsearch.common.StopWatch;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.script.ExecutableScript;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ */
+public class SimpleBench {
+
+    public static void main(String[] args) {
+        JavaScriptScriptEngineService se = new JavaScriptScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
+        Object compiled = se.compile("x + y");
+
+        Map<String, Object> vars = new HashMap<String, Object>();
+        // warm up
+        for (int i = 0; i < 1000; i++) {
+            vars.put("x", i);
+            vars.put("y", i + 1);
+            se.execute(compiled, vars);
+        }
+
+        final long ITER = 100000;
+
+        StopWatch stopWatch = new StopWatch().start();
+        for (long i = 0; i < ITER; i++) {
+            se.execute(compiled, vars);
+        }
+        System.out.println("Execute Took: " + stopWatch.stop().lastTaskTime());
+
+        stopWatch = new StopWatch().start();
+        ExecutableScript executableScript = se.executable(compiled, vars);
+        for (long i = 0; i < ITER; i++) {
+            executableScript.run();
+        }
+        System.out.println("Executable Took: " + stopWatch.stop().lastTaskTime());
+
+        stopWatch = new StopWatch().start();
+        executableScript = se.executable(compiled, vars);
+        for (long i = 0; i < ITER; i++) {
+            for (Map.Entry<String, Object> entry : vars.entrySet()) {
+                executableScript.setNextVar(entry.getKey(), entry.getValue());
+            }
+            executableScript.run();
+        }
+        System.out.println("Executable (vars) Took: " + stopWatch.stop().lastTaskTime());
+    }
+}