implements TransportFutu
}
@Override public void handleResponse(V response) {
- this.done = true;
- this.result = response;
-
- if (canceled)
- return;
-
handler.handleResponse(response);
- latch.countDown();
+ set(response);
}
@Override public void handleException(RemoteTransportException exp) {
- this.done = true;
- this.exp = exp;
-
- if (canceled)
- return;
-
handler.handleException(exp);
- latch.countDown();
+ setException(exp);
}
@Override public boolean spawn() {
diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/concurrent/AbstractFuture.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/concurrent/AbstractFuture.java
new file mode 100644
index 00000000000..ebc15d21d22
--- /dev/null
+++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/concurrent/AbstractFuture.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to Elastic Search and Shay Banon under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Elastic Search 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.util.concurrent;
+
+import java.util.concurrent.*;
+import java.util.concurrent.locks.AbstractQueuedSynchronizer;
+
+/**
+ * An abstract implementation of the {@link Future} interface. This class
+ * is an abstraction of {@link java.util.concurrent.FutureTask} to support use
+ * for tasks other than {@link Runnable}s. It uses an
+ * {@link AbstractQueuedSynchronizer} to deal with concurrency issues and
+ * guarantee thread safety. It could be used as a base class to
+ * {@code FutureTask}, or any other implementor of the {@code Future} interface.
+ *
+ *
This class implements all methods in {@code Future}. Subclasses should
+ * provide a way to set the result of the computation through the protected
+ * methods {@link #set(Object)}, {@link #setException(Throwable)}, or
+ * {@link #cancel()}. If subclasses want to implement cancellation they can
+ * override the {@link #cancel(boolean)} method with a real implementation, the
+ * default implementation doesn't support cancellation.
+ *
+ *
The state changing methods all return a boolean indicating success or
+ * failure in changing the future's state. Valid states are running,
+ * completed, failed, or cancelled. Because this class does not implement
+ * cancellation it is left to the subclass to distinguish between created
+ * and running tasks.
+ */
+public abstract class AbstractFuture implements Future {
+
+ /**
+ * Synchronization control for AbstractFutures.
+ */
+ private final Sync sync = new Sync();
+
+ /*
+ * Blocks until either the task completes or the timeout expires. Uses the
+ * sync blocking-with-timeout support provided by AQS.
+ */
+
+ public V get(long timeout, TimeUnit unit) throws InterruptedException,
+ TimeoutException, ExecutionException {
+ return sync.get(unit.toNanos(timeout));
+ }
+
+ /*
+ * Blocks until the task completes or we get interrupted. Uses the
+ * interruptible blocking support provided by AQS.
+ */
+
+ public V get() throws InterruptedException, ExecutionException {
+ return sync.get();
+ }
+
+ /*
+ * Checks if the sync is not in the running state.
+ */
+
+ public boolean isDone() {
+ return sync.isDone();
+ }
+
+ /*
+ * Checks if the sync is in the cancelled state.
+ */
+
+ public boolean isCancelled() {
+ return sync.isCancelled();
+ }
+
+ /*
+ * Default implementation of cancel that never cancels the future.
+ * Subclasses should override this to implement cancellation if desired.
+ */
+
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ /**
+ * Subclasses should invoke this method to set the result of the computation
+ * to {@code value}. This will set the state of the future to
+ * {@link AbstractFuture.Sync#COMPLETED} and call {@link #done()} if the
+ * state was successfully changed.
+ *
+ * @param value the value that was the result of the task.
+ * @return true if the state was successfully changed.
+ */
+ protected boolean set(V value) {
+ boolean result = sync.set(value);
+ if (result) {
+ done();
+ }
+ return result;
+ }
+
+ /**
+ * Subclasses should invoke this method to set the result of the computation
+ * to an error, {@code throwable}. This will set the state of the future to
+ * {@link AbstractFuture.Sync#COMPLETED} and call {@link #done()} if the
+ * state was successfully changed.
+ *
+ * @param throwable the exception that the task failed with.
+ * @return true if the state was successfully changed.
+ * @throws Error if the throwable was an {@link Error}.
+ */
+ protected boolean setException(Throwable throwable) {
+ boolean result = sync.setException(throwable);
+ if (result) {
+ done();
+ }
+
+ // If it's an Error, we want to make sure it reaches the top of the
+ // call stack, so we rethrow it.
+ if (throwable instanceof Error) {
+ throw (Error) throwable;
+ }
+ return result;
+ }
+
+ /**
+ * Subclasses should invoke this method to mark the future as cancelled.
+ * This will set the state of the future to {@link
+ * AbstractFuture.Sync#CANCELLED} and call {@link #done()} if the state was
+ * successfully changed.
+ *
+ * @return true if the state was successfully changed.
+ */
+ protected final boolean cancel() {
+ boolean result = sync.cancel();
+ if (result) {
+ done();
+ }
+ return result;
+ }
+
+ /*
+ * Called by the success, failed, or cancelled methods to indicate that the
+ * value is now available and the latch can be released. Subclasses can
+ * use this method to deal with any actions that should be undertaken when
+ * the task has completed.
+ */
+
+ protected void done() {
+ // Default implementation does nothing.
+ }
+
+ /**
+ * Following the contract of {@link AbstractQueuedSynchronizer} we create a
+ * private subclass to hold the synchronizer. This synchronizer is used to
+ * implement the blocking and waiting calls as well as to handle state changes
+ * in a thread-safe manner. The current state of the future is held in the
+ * Sync state, and the lock is released whenever the state changes to either
+ * {@link #COMPLETED} or {@link #CANCELLED}.
+ *
+ *
To avoid races between threads doing release and acquire, we transition
+ * to the final state in two steps. One thread will successfully CAS from
+ * RUNNING to COMPLETING, that thread will then set the result of the
+ * computation, and only then transition to COMPLETED or CANCELLED.
+ *
+ *
We don't use the integer argument passed between acquire methods so we
+ * pass around a -1 everywhere.
+ */
+ static final class Sync extends AbstractQueuedSynchronizer {
+
+ private static final long serialVersionUID = 0L;
+
+ /* Valid states. */
+ static final int RUNNING = 0;
+ static final int COMPLETING = 1;
+ static final int COMPLETED = 2;
+ static final int CANCELLED = 4;
+
+ private V value;
+ private ExecutionException exception;
+
+ /*
+ * Acquisition succeeds if the future is done, otherwise it fails.
+ */
+
+ @Override
+ protected int tryAcquireShared(int ignored) {
+ if (isDone()) {
+ return 1;
+ }
+ return -1;
+ }
+
+ /*
+ * We always allow a release to go through, this means the state has been
+ * successfully changed and the result is available.
+ */
+
+ @Override
+ protected boolean tryReleaseShared(int finalState) {
+ setState(finalState);
+ return true;
+ }
+
+ /**
+ * Blocks until the task is complete or the timeout expires. Throws a
+ * {@link TimeoutException} if the timer expires, otherwise behaves like
+ * {@link #get()}.
+ */
+ V get(long nanos) throws TimeoutException, CancellationException,
+ ExecutionException, InterruptedException {
+
+ // Attempt to acquire the shared lock with a timeout.
+ if (!tryAcquireSharedNanos(-1, nanos)) {
+ throw new TimeoutException("Timeout waiting for task.");
+ }
+
+ return getValue();
+ }
+
+ /**
+ * Blocks until {@link #complete(Object, Throwable, int)} has been
+ * successfully called. Throws a {@link CancellationException} if the task
+ * was cancelled, or a {@link ExecutionException} if the task completed with
+ * an error.
+ */
+ V get() throws CancellationException, ExecutionException,
+ InterruptedException {
+
+ // Acquire the shared lock allowing interruption.
+ acquireSharedInterruptibly(-1);
+ return getValue();
+ }
+
+ /**
+ * Implementation of the actual value retrieval. Will return the value
+ * on success, an exception on failure, a cancellation on cancellation, or
+ * an illegal state if the synchronizer is in an invalid state.
+ */
+ private V getValue() throws CancellationException, ExecutionException {
+ int state = getState();
+ switch (state) {
+ case COMPLETED:
+ if (exception != null) {
+ throw exception;
+ } else {
+ return value;
+ }
+
+ case CANCELLED:
+ throw new CancellationException("Task was cancelled.");
+
+ default:
+ throw new IllegalStateException(
+ "Error, synchronizer in invalid state: " + state);
+ }
+ }
+
+ /**
+ * Checks if the state is {@link #COMPLETED} or {@link #CANCELLED}.
+ */
+ boolean isDone() {
+ return (getState() & (COMPLETED | CANCELLED)) != 0;
+ }
+
+ /**
+ * Checks if the state is {@link #CANCELLED}.
+ */
+ boolean isCancelled() {
+ return getState() == CANCELLED;
+ }
+
+ /**
+ * Transition to the COMPLETED state and set the value.
+ */
+ boolean set(V v) {
+ return complete(v, null, COMPLETED);
+ }
+
+ /**
+ * Transition to the COMPLETED state and set the exception.
+ */
+ boolean setException(Throwable t) {
+ return complete(null, t, COMPLETED);
+ }
+
+ /**
+ * Transition to the CANCELLED state.
+ */
+ boolean cancel() {
+ return complete(null, null, CANCELLED);
+ }
+
+ /**
+ * Implementation of completing a task. Either {@code v} or {@code t} will
+ * be set but not both. The {@code finalState} is the state to change to
+ * from {@link #RUNNING}. If the state is not in the RUNNING state we
+ * return {@code false}.
+ *
+ * @param v the value to set as the result of the computation.
+ * @param t the exception to set as the result of the computation.
+ * @param finalState the state to transition to.
+ */
+ private boolean complete(V v, Throwable t, int finalState) {
+ if (compareAndSetState(RUNNING, COMPLETING)) {
+ this.value = v;
+ this.exception = t == null ? null : new ExecutionException(t);
+ releaseShared(finalState);
+ return true;
+ }
+
+ // The state was not RUNNING, so there are no valid transitions.
+ return false;
+ }
+ }
+}
diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/BinaryJsonBuilder.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/BinaryJsonBuilder.java
index 3c863006eb1..b72935ea2e2 100644
--- a/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/BinaryJsonBuilder.java
+++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/BinaryJsonBuilder.java
@@ -65,8 +65,6 @@ public class BinaryJsonBuilder extends JsonBuilder {
private final JsonFactory factory;
- private StringBuilder cachedStringBuilder;
-
public BinaryJsonBuilder() throws IOException {
this(Jackson.defaultJsonFactory());
}
@@ -85,10 +83,6 @@ public class BinaryJsonBuilder extends JsonBuilder {
this.builder = this;
}
- @Override protected StringBuilder cachedStringBuilder() {
- return cachedStringBuilder;
- }
-
@Override public BinaryJsonBuilder raw(byte[] json) throws IOException {
flush();
bos.write(json);
diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/JsonBuilder.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/JsonBuilder.java
index d3e4c7649df..8b447b379d1 100644
--- a/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/JsonBuilder.java
+++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/JsonBuilder.java
@@ -69,6 +69,8 @@ public abstract class JsonBuilder {
protected FieldCaseConversion fieldCaseConversion = globalFieldCaseConversion;
+ protected StringBuilder cachedStringBuilder;
+
public static StringJsonBuilder stringJsonBuilder() throws IOException {
return StringJsonBuilder.Cached.cached();
}
@@ -143,9 +145,9 @@ public abstract class JsonBuilder {
public T field(String name) throws IOException {
if (fieldCaseConversion == FieldCaseConversion.UNDERSCORE) {
- name = Strings.toUnderscoreCase(name);
+ name = Strings.toUnderscoreCase(name, cachedStringBuilder);
} else if (fieldCaseConversion == FieldCaseConversion.CAMELCASE) {
- name = Strings.toCamelCase(name);
+ name = Strings.toCamelCase(name, cachedStringBuilder);
}
generator.writeFieldName(name);
return builder;
@@ -403,10 +405,6 @@ public abstract class JsonBuilder {
public abstract String string() throws IOException;
- protected StringBuilder cachedStringBuilder() {
- return null;
- }
-
public void close() {
try {
generator.close();
diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/StringJsonBuilder.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/StringJsonBuilder.java
index bf7e1f78188..c96520cf9fc 100644
--- a/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/StringJsonBuilder.java
+++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/json/StringJsonBuilder.java
@@ -69,8 +69,6 @@ public class StringJsonBuilder extends JsonBuilder {
final UnicodeUtil.UTF8Result utf8Result = new UnicodeUtil.UTF8Result();
- private StringBuilder cachedStringBuilder;
-
public StringJsonBuilder() throws IOException {
this(Jackson.defaultJsonFactory());
}
@@ -89,10 +87,6 @@ public class StringJsonBuilder extends JsonBuilder {
this.builder = this;
}
- @Override protected StringBuilder cachedStringBuilder() {
- return cachedStringBuilder;
- }
-
@Override public StringJsonBuilder raw(byte[] json) throws IOException {
flush();
Unicode.UTF16Result result = Unicode.unsafeFromBytesAsUtf16(json);
diff --git a/plugins/attachments/build.gradle b/plugins/attachments/build.gradle
index a4fbcc2567f..4d878e084fc 100644
--- a/plugins/attachments/build.gradle
+++ b/plugins/attachments/build.gradle
@@ -107,15 +107,12 @@ artifacts {
uploadArchives {
repositories.mavenDeployer {
-// repository(url: "file://localhost/" + rootProject.projectDir.absolutePath + "/build/maven/repository")
-// snapshotRepository(url: "file://localhost/" + rootProject.projectDir.absolutePath + "/build/maven/snapshotRepository")
-
configuration = configurations.deployerJars
- repository(url: "http://oss.sonatype.org/service/local/staging/deploy/maven2/") {
- authentication(userName: "kimchy", password: System.getenv("REPO_PASS"))
+ repository(url: rootProject.mavenRepoUrl) {
+ authentication(userName: rootProject.mavenRepoUser, password: rootProject.mavenRepoPass)
}
- snapshotRepository(url: "http://oss.sonatype.org/content/repositories/snapshots") {
- authentication(userName: "kimchy", password: System.getenv("REPO_PASS"))
+ snapshotRepository(url: rootProject.mavenSnapshotRepoUrl) {
+ authentication(userName: rootProject.mavenRepoUser, password: rootProject.mavenRepoPass)
}
pom.project {
diff --git a/plugins/groovy/build.gradle b/plugins/groovy/build.gradle
new file mode 100644
index 00000000000..ed477725287
--- /dev/null
+++ b/plugins/groovy/build.gradle
@@ -0,0 +1,138 @@
+dependsOn(':elasticsearch')
+
+apply plugin: 'groovy'
+apply plugin: 'maven'
+
+archivesBaseName = "elasticsearch-groovy"
+
+explodedDistDir = new File(distsDir, 'exploded')
+
+manifest.mainAttributes("Implementation-Title": "ElasticSearch::Plugins::Groovy", "Implementation-Version": rootProject.version, "Implementation-Date": buildTimeStr)
+
+configurations.compile.transitive = true
+configurations.testCompile.transitive = true
+
+// no need to use the resource dir
+sourceSets.main.resources.srcDir 'src/main/groovy'
+sourceSets.test.resources.srcDir 'src/test/groovy'
+
+// add the source files to the dist jar
+jar {
+ from sourceSets.main.allSource
+}
+
+configurations {
+ dists
+ distLib {
+ visible = false
+ transitive = false
+ }
+}
+
+dependencies {
+ compile project(':elasticsearch')
+
+ groovy group: 'org.codehaus.groovy', name: 'groovy', version: '1.7.2'
+
+ testCompile project(':test-testng')
+ testCompile('org.testng:testng:5.10:jdk15') { transitive = false }
+ testCompile 'org.hamcrest:hamcrest-all:1.1'
+}
+
+test {
+ useTestNG()
+ jmvArgs = ["-ea", "-Xmx1024m"]
+ suiteName = project.name
+ listeners = ["org.elasticsearch.util.testng.Listeners"]
+ systemProperties["es.test.log.conf"] = System.getProperty("es.test.log.conf", "log4j-gradle.properties")
+}
+
+task explodedDist(dependsOn: [jar], description: 'Builds the plugin zip file') << {
+ [explodedDistDir]*.mkdirs()
+
+ copy {
+ from configurations.distLib
+ into explodedDistDir
+ }
+
+ // remove elasticsearch files (compile above adds the elasticsearch one)
+ ant.delete { fileset(dir: explodedDistDir, includes: "elasticsearch-*.jar") }
+
+ copy {
+ from libsDir
+ into explodedDistDir
+ }
+
+ ant.delete { fileset(dir: explodedDistDir, includes: "elasticsearch-*-javadoc.jar") }
+ ant.delete { fileset(dir: explodedDistDir, includes: "elasticsearch-*-sources.jar") }
+}
+
+task zip(type: Zip, dependsOn: ['explodedDist']) {
+ from(explodedDistDir) {
+ }
+}
+
+task release(dependsOn: [zip]) << {
+ ant.delete(dir: explodedDistDir)
+ copy {
+ from distsDir
+ into(new File(rootProject.distsDir, "plugins"))
+ }
+}
+
+configurations {
+ deployerJars
+}
+
+dependencies {
+ deployerJars "org.apache.maven.wagon:wagon-http:1.0-beta-2"
+}
+
+task sourcesJar(type: Jar, dependsOn: classes) {
+ classifier = 'sources'
+ from sourceSets.main.allSource
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+}
+
+artifacts {
+ archives sourcesJar
+ archives javadocJar
+}
+
+uploadArchives {
+ repositories.mavenDeployer {
+ configuration = configurations.deployerJars
+ repository(url: rootProject.mavenRepoUrl) {
+ authentication(userName: rootProject.mavenRepoUser, password: rootProject.mavenRepoPass)
+ }
+ snapshotRepository(url: rootProject.mavenSnapshotRepoUrl) {
+ authentication(userName: rootProject.mavenRepoUser, password: rootProject.mavenRepoPass)
+ }
+
+ pom.project {
+ inceptionYear '2009'
+ name 'elasticsearch-plugins-groovy'
+ description 'Groovy Plugin for ElasticSearch'
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+ scm {
+ connection 'git://github.com/elasticsearch/elasticsearch.git'
+ developerConnection 'git@github.com:elasticsearch/elasticsearch.git'
+ url 'http://github.com/elasticsearch/elasticsearch'
+ }
+ }
+
+ pom.whenConfigured {pom ->
+ pom.dependencies = pom.dependencies.findAll {dep -> dep.scope != 'test' } // removes the test scoped ones
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GAdminClient.groovy b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GAdminClient.groovy
new file mode 100644
index 00000000000..29b539037d7
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GAdminClient.groovy
@@ -0,0 +1,22 @@
+package org.elasticsearch.groovy.client
+
+import org.elasticsearch.client.internal.InternalClient
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class GAdminClient {
+
+ private final InternalClient internalClient;
+
+ final GIndicesAdminClient indices;
+
+ final GClusterAdminClient cluster;
+
+ def GAdminClient(internalClient) {
+ this.internalClient = internalClient;
+
+ this.indices = new GIndicesAdminClient(internalClient)
+ this.cluster = new GClusterAdminClient(internalClient)
+ }
+}
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GClient.groovy b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GClient.groovy
new file mode 100644
index 00000000000..c68bb74310c
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GClient.groovy
@@ -0,0 +1,111 @@
+/*
+ * Licensed to Elastic Search and Shay Banon under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Elastic Search 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.groovy.client
+
+import org.elasticsearch.action.ActionListener
+import org.elasticsearch.action.delete.DeleteRequest
+import org.elasticsearch.action.delete.DeleteResponse
+import org.elasticsearch.action.get.GetRequest
+import org.elasticsearch.action.get.GetResponse
+import org.elasticsearch.action.index.IndexRequest
+import org.elasticsearch.action.index.IndexResponse
+import org.elasticsearch.client.Client
+import org.elasticsearch.client.internal.InternalClient
+import org.elasticsearch.groovy.client.action.GActionFuture
+import org.elasticsearch.groovy.util.json.JsonBuilder
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class GClient {
+
+ static {
+ IndexRequest.metaClass.setSource = {Closure c ->
+ delegate.source(new JsonBuilder().buildAsBytes(c))
+ }
+ IndexRequest.metaClass.source = {Closure c ->
+ delegate.source(new JsonBuilder().buildAsBytes(c))
+ }
+ }
+
+ final Client client;
+
+ private final InternalClient internalClient
+
+ final GAdminClient admin;
+
+ def GClient(client) {
+ this.client = client;
+ this.internalClient = client;
+
+ this.admin = new GAdminClient(internalClient)
+ }
+
+ GActionFuture index(Closure c) {
+ IndexRequest request = new IndexRequest()
+ c.setDelegate request
+ c.call()
+ index(request)
+ }
+
+ GActionFuture index(IndexRequest request) {
+ GActionFuture future = new GActionFuture(internalClient.threadPool(), request);
+ client.index(request, future)
+ return future
+ }
+
+ void index(IndexRequest request, ActionListener listener) {
+ client.index(request, listener)
+ }
+
+ GActionFuture get(Closure c) {
+ GetRequest request = new GetRequest()
+ c.setDelegate request
+ c.call()
+ get(request)
+ }
+
+ GActionFuture get(GetRequest request) {
+ GActionFuture future = new GActionFuture(internalClient.threadPool(), request);
+ client.get(request, future)
+ return future
+ }
+
+ void get(GetRequest request, ActionListener listener) {
+ client.get(request, listener)
+ }
+
+ GActionFuture delete(Closure c) {
+ DeleteRequest request = new DeleteRequest()
+ c.setDelegate request
+ c.call()
+ delete(request)
+ }
+
+ GActionFuture delete(DeleteRequest request) {
+ GActionFuture future = new GActionFuture(internalClient.threadPool(), request);
+ client.delete(request, future)
+ return future
+ }
+
+ void delete(DeleteRequest request, ActionListener listener) {
+ client.delete(request, listener)
+ }
+}
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GClusterAdminClient.groovy b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GClusterAdminClient.groovy
new file mode 100644
index 00000000000..7d0e876a259
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GClusterAdminClient.groovy
@@ -0,0 +1,19 @@
+package org.elasticsearch.groovy.client
+
+import org.elasticsearch.client.ClusterAdminClient
+import org.elasticsearch.client.internal.InternalClient
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class GClusterAdminClient {
+
+ private final InternalClient internalClient;
+
+ private final ClusterAdminClient clusterAdminClient;
+
+ def GClusterAdminClient(internalClient) {
+ this.internalClient = internalClient;
+ this.clusterAdminClient = internalClient.admin().cluster();
+ }
+}
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GIndicesAdminClient.groovy b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GIndicesAdminClient.groovy
new file mode 100644
index 00000000000..238ac9b846a
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/GIndicesAdminClient.groovy
@@ -0,0 +1,40 @@
+package org.elasticsearch.groovy.client
+
+import org.elasticsearch.action.ActionListener
+import org.elasticsearch.action.admin.indices.refresh.RefreshRequest
+import org.elasticsearch.action.admin.indices.refresh.RefreshResponse
+import org.elasticsearch.client.IndicesAdminClient
+import org.elasticsearch.client.internal.InternalClient
+import org.elasticsearch.groovy.client.action.GActionFuture
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class GIndicesAdminClient {
+
+ private final InternalClient internalClient;
+
+ private final IndicesAdminClient indicesAdminClient;
+
+ def GIndicesAdminClient(internalClient) {
+ this.internalClient = internalClient;
+ this.indicesAdminClient = internalClient.admin().indices();
+ }
+
+ GActionFuture refresh(Closure c) {
+ RefreshRequest request = new RefreshRequest()
+ c.setDelegate request
+ c.call()
+ refresh(request)
+ }
+
+ GActionFuture refresh(RefreshRequest request) {
+ GActionFuture future = new GActionFuture(internalClient.threadPool(), request);
+ indicesAdminClient.refresh(request, future)
+ return future
+ }
+
+ void refresh(RefreshRequest request, ActionListener listener) {
+ indicesAdminClient.refresh(request, listener)
+ }
+}
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/action/GActionFuture.java b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/action/GActionFuture.java
new file mode 100644
index 00000000000..2f64a49aba6
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/client/action/GActionFuture.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to Elastic Search and Shay Banon under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Elastic Search 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.groovy.client.action;
+
+import groovy.lang.Closure;
+import org.elasticsearch.ElasticSearchException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.support.PlainListenableActionFuture;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.util.TimeValue;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author kimchy (shay.banon)
+ */
+public class GActionFuture extends PlainListenableActionFuture {
+
+ protected GActionFuture(ThreadPool threadPool, ActionRequest request) {
+ super(request.listenerThreaded(), threadPool);
+ }
+
+ public void setListener(final Closure listener) {
+ addListener(new ActionListener() {
+ @Override public void onResponse(T t) {
+ listener.call(this);
+ }
+
+ @Override public void onFailure(Throwable e) {
+ listener.call(this);
+ }
+ });
+ }
+
+ public void setSuccess(final Closure success) {
+ addListener(new ActionListener() {
+ @Override public void onResponse(T t) {
+ success.call(t);
+ }
+
+ @Override public void onFailure(Throwable e) {
+ // ignore
+ }
+ });
+ }
+
+ public void setFailure(final Closure failure) {
+ addListener(new ActionListener() {
+ @Override public void onResponse(T t) {
+ // nothing
+ }
+
+ @Override public void onFailure(Throwable e) {
+ failure.call(e);
+ }
+ });
+ }
+
+ public T getResponse() {
+ return actionGet();
+ }
+
+ public T response(String timeout) throws ElasticSearchException {
+ return super.actionGet(timeout);
+ }
+
+ public T response(long timeoutMillis) throws ElasticSearchException {
+ return super.actionGet(timeoutMillis);
+ }
+
+ public T response(TimeValue timeout) throws ElasticSearchException {
+ return super.actionGet(timeout);
+ }
+
+ public T response(long timeout, TimeUnit unit) throws ElasticSearchException {
+ return super.actionGet(timeout, unit);
+ }
+}
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/node/GNode.groovy b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/node/GNode.groovy
new file mode 100644
index 00000000000..35d59f5428d
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/node/GNode.groovy
@@ -0,0 +1,50 @@
+package org.elasticsearch.groovy.node
+
+import org.elasticsearch.groovy.client.GClient
+import org.elasticsearch.node.Node
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class GNode {
+
+ final Node node;
+
+ final GClient client;
+
+ def GNode(Node node) {
+ this.node = node;
+ this.client = new GClient(node.client())
+ }
+
+ /**
+ * The settings that were used to create the node.
+ */
+ def getSettings() {
+ node.settings();
+ }
+
+ /**
+ * Start the node. If the node is already started, this method is no-op.
+ */
+ def getStart() {
+ node.start()
+ this
+ }
+
+ /**
+ * Stops the node. If the node is already started, this method is no-op.
+ */
+ def getStop() {
+ node.stop()
+ this
+ }
+
+ /**
+ * Closes the node (and {@link #stop} s if its running).
+ */
+ def getClose() {
+ node.close()
+ this
+ }
+}
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/node/GNodeBuilder.groovy b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/node/GNodeBuilder.groovy
new file mode 100644
index 00000000000..a363623631b
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/node/GNodeBuilder.groovy
@@ -0,0 +1,35 @@
+package org.elasticsearch.groovy.node
+
+import org.elasticsearch.groovy.util.json.JsonBuilder
+import org.elasticsearch.node.Node
+import org.elasticsearch.node.internal.InternalNode
+import org.elasticsearch.util.settings.ImmutableSettings
+import org.elasticsearch.util.settings.loader.JsonSettingsLoader
+
+/**
+ * @author kimchy (shay.banon)
+ */
+public class GNodeBuilder {
+
+ private final ImmutableSettings.Builder settingsBuilder = ImmutableSettings.settingsBuilder();
+
+ private boolean loadConfigSettings = true;
+
+ public static GNodeBuilder nodeBuilder() {
+ new GNodeBuilder()
+ }
+
+ def settings(Closure settings) {
+ byte[] settingsBytes = new JsonBuilder().buildAsBytes(settings);
+ settingsBuilder.put(new JsonSettingsLoader().load(settingsBytes))
+ }
+
+ def getBuild() {
+ Node node = new InternalNode(settingsBuilder.build(), loadConfigSettings)
+ new GNode(node)
+ }
+
+ def getNode() {
+ build.start
+ }
+}
diff --git a/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/util/json/JsonBuilder.groovy b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/util/json/JsonBuilder.groovy
new file mode 100644
index 00000000000..1d31817cfbd
--- /dev/null
+++ b/plugins/groovy/src/main/groovy/org/elasticsearch/groovy/util/json/JsonBuilder.groovy
@@ -0,0 +1,176 @@
+package org.elasticsearch.groovy.util.json
+
+import org.elasticsearch.ElasticSearchGenerationException
+import org.elasticsearch.util.io.FastByteArrayOutputStream
+import org.elasticsearch.util.io.FastCharArrayWriter
+import static org.elasticsearch.util.json.Jackson.*
+
+/**
+ * Used to build JSON data.
+ *
+ * @author Marc Palmer
+ * @author Graeme Rocher
+ *
+ * @since 1.2
+ */
+class JsonBuilder {
+
+ static NODE_ELEMENT = "element"
+
+ def root
+
+ def current
+
+ def nestingStack = []
+
+ def build(Closure c) {
+ return buildRoot(c)
+ }
+
+ String buildAsString(Closure c) {
+ FastCharArrayWriter writer = FastCharArrayWriter.Cached.cached();
+ try {
+ def json = build(c)
+ defaultObjectMapper().writeValue(writer, json);
+ } catch (IOException e) {
+ throw new ElasticSearchGenerationException("Failed to generate [" + c + "]", e);
+ }
+ return writer.toStringTrim()
+ }
+
+ byte[] buildAsBytes(Closure c) {
+ FastByteArrayOutputStream os = FastByteArrayOutputStream.Cached.cached();
+ try {
+ def json = build(c)
+ defaultObjectMapper().writeValue(os, json);
+ } catch (IOException e) {
+ throw new ElasticSearchGenerationException("Failed to generate [" + c + "]", e);
+ }
+ return os.copiedByteArray()
+ }
+
+ private buildRoot(Closure c) {
+ c.delegate = this
+ //c.resolveStrategy = Closure.DELEGATE_FIRST
+ root = [:]
+ current = root
+ def returnValue = c.call()
+ if (!root) {
+ return returnValue
+ }
+ return root
+ }
+
+ def invokeMethod(String methodName) {
+ current[methodName] = []
+ }
+
+ List array(Closure c) {
+ def prev = current
+ def list = []
+ try {
+
+ current = list
+ c.call(list)
+ }
+ finally {
+ current = prev
+ }
+ return list
+ }
+
+ def invokeMethod(String methodName, Object args) {
+ if (args.size()) {
+ if (args[0] instanceof Map) {
+ // switch root to an array if elements used at top level
+ if ((current == root) && (methodName == NODE_ELEMENT) && !(root instanceof List)) {
+ if (root.size()) {
+ throw new IllegalArgumentException('Cannot have array elements in root node if properties of root have already been set')
+ } else {
+ root = []
+ current = root
+ }
+ }
+ def n = [:]
+ if (current instanceof List) {
+ current << n
+ } else {
+ current[methodName] = n
+ }
+ n.putAll(args[0])
+ } else if (args[-1] instanceof Closure) {
+ final Object callable = args[-1]
+ handleClosureNode(methodName, callable)
+ } else if (args.size() == 1) {
+ if (methodName != NODE_ELEMENT) {
+ throw new IllegalArgumentException('Array elements must be defined with the "element" method call eg: element(value)')
+ }
+ // switch root to an array if elements used at top level
+ if (current == root) {
+ if (root.size() && methodName != NODE_ELEMENT) {
+ throw new IllegalArgumentException('Cannot have array elements in root node if properties of root have already been set')
+ } else if (!(root instanceof List)) {
+ root = []
+ current = root
+ }
+ }
+ if (current instanceof List) {
+ current << args[0]
+ } else {
+ throw new IllegalArgumentException('Array elements can only be defined under "array" nodes')
+ }
+ } else {
+ throw new IllegalArgumentException("This builder does not support invocation of [$methodName] with arg list ${args.dump()}")
+ }
+ } else {
+ current[methodName] = []
+ }
+ }
+
+ private handleClosureNode(String methodName, callable) {
+ def n = [:]
+ nestingStack << current
+
+ if (current instanceof List) {
+ current << n
+ }
+ else {
+ current[methodName] = n
+ }
+ current = n
+ callable.call()
+ current = nestingStack.pop()
+ }
+
+
+ void setProperty(String propName, Object value) {
+ if (value instanceof Closure) {
+ handleClosureNode(propName, value)
+ }
+ else if (value instanceof List) {
+ value = value.collect {
+ if (it instanceof Closure) {
+ def callable = it
+ final JsonBuilder localBuilder = new JsonBuilder()
+ callable.delegate = localBuilder
+ callable.resolveStrategy = Closure.DELEGATE_FIRST
+ final Map nestedObject = localBuilder.buildRoot(callable)
+ return nestedObject
+ }
+ else {
+ return it
+ }
+
+ }
+ current[propName] = value
+ }
+ else {
+ current[propName] = value
+ }
+ }
+
+ def getProperty(String propName) {
+ current[propName]
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/groovy/src/test/groovy/log4j.properties b/plugins/groovy/src/test/groovy/log4j.properties
new file mode 100644
index 00000000000..86d72561ccf
--- /dev/null
+++ b/plugins/groovy/src/test/groovy/log4j.properties
@@ -0,0 +1,21 @@
+log4j.rootLogger=INFO, out
+log4j.logger.jgroups=WARN
+
+#log4j.logger.discovery=TRACE
+#log4j.logger.cluster.service=TRACE
+#log4j.logger.cluster.action.shard=DEBUG
+#log4j.logger.indices.cluster=DEBUG
+#log4j.logger.index=TRACE
+#log4j.logger.index.engine=DEBUG
+#log4j.logger.index.shard.service=DEBUG
+#log4j.logger.index.shard.recovery=DEBUG
+#log4j.logger.index.cache=DEBUG
+#log4j.logger.http=TRACE
+#log4j.logger.monitor.memory=TRACE
+#log4j.logger.monitor.memory=TRACE
+#log4j.logger.cluster.action.shard=TRACE
+#log4j.logger.index.gateway=TRACE
+
+log4j.appender.out=org.apache.log4j.ConsoleAppender
+log4j.appender.out.layout=org.apache.log4j.PatternLayout
+log4j.appender.out.layout.ConversionPattern=[%d{ABSOLUTE}][%-5p][%-25c] %m%n
diff --git a/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/test/client/SimpleActionsTests.groovy b/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/test/client/SimpleActionsTests.groovy
new file mode 100644
index 00000000000..dee75dd97c9
--- /dev/null
+++ b/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/test/client/SimpleActionsTests.groovy
@@ -0,0 +1,136 @@
+package org.elasticsearch.groovy.test.client
+
+import java.util.concurrent.CountDownLatch
+import org.elasticsearch.action.index.IndexRequest
+import org.elasticsearch.action.index.IndexResponse
+import org.elasticsearch.groovy.node.GNode
+import org.elasticsearch.groovy.node.GNodeBuilder
+import static org.elasticsearch.client.Requests.*
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class SimpleActionsTests extends GroovyTestCase {
+
+ void testSimpleOperations() {
+ GNodeBuilder nodeBuilder = new GNodeBuilder()
+ nodeBuilder.settings {
+ node {
+ local = true
+ }
+ }
+
+ GNode node = nodeBuilder.node
+
+ def response = node.client.index(new IndexRequest(
+ index: "test",
+ type: "type1",
+ id: "1",
+ source: {
+ test = "value"
+ complex {
+ value1 = "value1"
+ value2 = "value2"
+ }
+ })).response
+ assertEquals "test", response.index
+ assertEquals "type1", response.type
+ assertEquals "1", response.id
+
+ def refresh = node.client.admin.indices.refresh {}
+ assertEquals 0, refresh.response.failedShards
+
+ def getR = node.client.get {
+ index "test"
+ type "type1"
+ id "1"
+ }
+ assertTrue getR.response.exists
+ assertEquals "test", getR.response.index
+ assertEquals "type1", getR.response.type
+ assertEquals "1", getR.response.id
+ assertEquals '{"test":"value","complex":{"value1":"value1","value2":"value2"}}', getR.response.sourceAsString()
+ assertEquals "value", getR.response.source.test
+ assertEquals "value1", getR.response.source.complex.value1
+
+ response = node.client.index({
+ index = "test"
+ type = "type1"
+ id = "1"
+ source = {
+ test = "value"
+ complex {
+ value1 = "value1"
+ value2 = "value2"
+ }
+ }
+ }).response
+ assertEquals "test", response.index
+ assertEquals "type1", response.type
+ assertEquals "1", response.id
+
+ def indexR = node.client.index(indexRequest().with {
+ index "test"
+ type "type1"
+ id "1"
+ source {
+ test = "value"
+ complex {
+ value1 = "value1"
+ value2 = "value2"
+ }
+ }
+ })
+ CountDownLatch latch = new CountDownLatch(1)
+ indexR.success = {IndexResponse responseX ->
+ assertEquals "test", responseX.index
+ assertEquals "test", indexR.response.index
+ assertEquals "type1", responseX.type
+ assertEquals "type1", indexR.response.type
+ assertEquals "1", responseX.id
+ assertEquals "1", indexR.response.id
+ latch.countDown()
+ }
+ latch.await()
+
+ indexR = node.client.index {
+ index "test"
+ type "type1"
+ id "1"
+ source {
+ test = "value"
+ complex {
+ value1 = "value1"
+ value2 = "value2"
+ }
+ }
+ }
+ latch = new CountDownLatch(1)
+ indexR.listener = {
+ assertEquals "test", indexR.response.index
+ assertEquals "type1", indexR.response.type
+ assertEquals "1", indexR.response.id
+ latch.countDown()
+ }
+ latch.await()
+
+ def delete = node.client.delete {
+ index "test"
+ type "type1"
+ id "1"
+ }
+ assertEquals "test", delete.response.index
+ assertEquals "type1", delete.response.type
+ assertEquals "1", delete.response.id
+
+ refresh = node.client.admin.indices.refresh {}
+ assertEquals 0, refresh.response.failedShards
+
+ getR = node.client.get {
+ index "test"
+ type "type1"
+ id "1"
+ }
+ assertFalse getR.response.exists
+ }
+}
diff --git a/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/test/node/GNodeBuilderTests.groovy b/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/test/node/GNodeBuilderTests.groovy
new file mode 100644
index 00000000000..10725f84a58
--- /dev/null
+++ b/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/test/node/GNodeBuilderTests.groovy
@@ -0,0 +1,25 @@
+package org.elasticsearch.groovy.test.node
+
+import org.elasticsearch.groovy.node.GNode
+import org.elasticsearch.groovy.node.GNodeBuilder
+import static org.elasticsearch.groovy.node.GNodeBuilder.*
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class GNodeBuilderTests extends GroovyTestCase {
+
+ void testGNodeBuilder() {
+ GNodeBuilder nodeBuilder = nodeBuilder();
+ nodeBuilder.settings {
+ node {
+ local = true
+ }
+ cluster {
+ name = "test"
+ }
+ }
+ GNode node = nodeBuilder.node
+ node.stop.close
+ }
+}
diff --git a/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/util/json/JsonBuilderTests.groovy b/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/util/json/JsonBuilderTests.groovy
new file mode 100644
index 00000000000..ed956526834
--- /dev/null
+++ b/plugins/groovy/src/test/groovy/org/elasticsearch/groovy/util/json/JsonBuilderTests.groovy
@@ -0,0 +1,148 @@
+package org.elasticsearch.groovy.util.json
+
+/**
+ * @author kimchy (shay.banon)
+ */
+class JsonBuilderTests extends GroovyTestCase {
+
+ void testSimple() {
+ def builder = new JsonBuilder()
+
+ def result = builder.buildAsString {
+ rootprop = "something"
+ }
+
+ assertEquals '{"rootprop":"something"}', result.toString()
+ }
+
+ void testArrays() {
+ def builder = new JsonBuilder()
+
+ def result = builder.buildAsString {
+ categories = ['a', 'b', 'c']
+ rootprop = "something"
+ }
+
+ assertEquals '{"categories":["a","b","c"],"rootprop":"something"}', result.toString()
+ }
+
+ void testSubObjects() {
+ def builder = new JsonBuilder()
+
+ def result = builder.buildAsString {
+ categories = ['a', 'b', 'c']
+ rootprop = "something"
+ test {
+ subprop = 10
+ }
+ }
+
+ assertEquals '{"categories":["a","b","c"],"rootprop":"something","test":{"subprop":10}}', result.toString()
+ }
+
+ void testAssignedObjects() {
+ def builder = new JsonBuilder()
+
+ def result = builder.buildAsString {
+ categories = ['a', 'b', 'c']
+ rootprop = "something"
+ test = {
+ subprop = 10
+ }
+ }
+
+ assertEquals '{"categories":["a","b","c"],"rootprop":"something","test":{"subprop":10}}', result.toString()
+ }
+
+ void testNamedArgumentHandling() {
+ def builder = new JsonBuilder()
+ def result = builder.buildAsString {
+ categories = ['a', 'b', 'c']
+ rootprop = "something"
+ test subprop: 10, three: [1, 2, 3]
+
+ }
+
+ assertEquals '{"categories":["a","b","c"],"rootprop":"something","test":{"subprop":10,"three":[1,2,3]}}', result.toString()
+ }
+
+
+ void testArrayOfClosures() {
+ def builder = new JsonBuilder()
+ def result = builder.buildAsString {
+ foo = [{ bar = "hello" }]
+ }
+
+ assertEquals '{"foo":[{"bar":"hello"}]}', result.toString()
+ }
+
+ void testRootElementList() {
+ def builder = new JsonBuilder()
+
+ def results = ['one', 'two', 'three']
+
+ def result = builder.buildAsString {
+ for (b in results) {
+ element b
+ }
+ }
+
+ assertEquals '["one","two","three"]', result.toString()
+
+ result = builder.buildAsString {
+ results
+ }
+
+ assertEquals '["one","two","three"]', result.toString()
+
+ }
+
+ void testExampleFromReferenceGuide() {
+ def builder = new JsonBuilder()
+
+ def results = ['one', 'two', 'three']
+
+ def result = builder.buildAsString {
+ for (b in results) {
+ element title: b
+ }
+ }
+
+ assertEquals '[{"title":"one"},{"title":"two"},{"title":"three"}]', result.toString()
+
+
+ result = builder.buildAsString {
+ books = results.collect {
+ [title: it]
+ }
+ }
+
+ assertEquals '{"books":[{"title":"one"},{"title":"two"},{"title":"three"}]}', result.toString()
+
+ result = builder.buildAsString {
+ books = array {
+ for (b in results) {
+ book title: b
+ }
+ }
+ }
+
+ assertEquals '{"books":[{"title":"one"},{"title":"two"},{"title":"three"}]}', result.toString()
+ }
+
+ void testAppendToArray() {
+ def builder = new JsonBuilder()
+
+ def results = ['one', 'two', 'three']
+
+ def result = builder.buildAsString {
+ books = array {list ->
+ for (b in results) {
+ list << [title: b]
+ }
+ }
+ }
+
+ assertEquals '{"books":[{"title":"one"},{"title":"two"},{"title":"three"}]}', result.toString()
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 63a2539b0e0..c7fbb21f713 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -7,6 +7,7 @@ include 'test-integration'
include 'benchmark-micro'
include 'plugins-attachments'
+include 'plugins-groovy'
rootProject.name = 'elasticsearch-root'
rootProject.children.each {project ->