SLING-7511 - Add importContent to SlingClient
diff --git a/pom.xml b/pom.xml
index 2eac034..10cc54f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -208,5 +208,11 @@
             <version>5.0.0</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/testing/clients/SlingClient.java b/src/main/java/org/apache/sling/testing/clients/SlingClient.java
index 8359bf2..7c4873d 100644
--- a/src/main/java/org/apache/sling/testing/clients/SlingClient.java
+++ b/src/main/java/org/apache/sling/testing/clients/SlingClient.java
@@ -475,6 +475,69 @@
     }
 
     /**
+     * <p>Create a tree structure under {@code parentPath} by providing a {@code content} in one
+     * of the supported formats: xml, jcr.xml, json, jar, zip.</p>
+     *
+     * <p>This is the implementation of {@code :operation import}, as documented in
+     * <a href="http://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html#importing-content-structures">importing-content-structures</a></p>
+     *
+     * @param parentPath path where the tree is created
+     * @param contentType format of the content
+     * @param content string expressing the structure to be created, in the specified format
+     * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed
+     * @return the response
+     * @throws ClientException if something fails during the request/response cycle
+     */
+    public SlingHttpResponse importContent(String parentPath, String contentType, String content, int... expectedStatus)
+            throws ClientException {
+        HttpEntity entity = FormEntityBuilder.create()
+                .addParameter(":operation", "import")
+                .addParameter(":contentType", contentType)
+                .addParameter(":content", content)
+                .build();
+        // execute request and return the sling response
+        return this.doPost(parentPath, entity, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+    }
+
+    /**
+     * <p>Create a tree structure under {@code parentPath} by providing a {@code contentFile} in one
+     * of the supported formats: xml, jcr.xml, json, jar, zip.</p>
+     *
+     * <p>This is the implementation of {@code :operation import}, as documented in
+     * <a href="http://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html#importing-content-structures">importing-content-structures</a></p>
+     *
+     * @param parentPath path where the tree is created
+     * @param contentType format of the content
+     * @param contentFile file containing the structure to be created, in the specified format
+     * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed
+     * @return the response
+     * @throws ClientException if something fails during the request/response cycle
+     */
+    public SlingHttpResponse importContent(String parentPath, String contentType, File contentFile, int... expectedStatus)
+            throws ClientException {
+        HttpEntity entity = MultipartEntityBuilder.create()
+                .addTextBody(":operation", "import")
+                .addTextBody(":contentType", contentType)
+                .addBinaryBody(":contentFile", contentFile)
+                .build();
+        // execute request and return the sling response
+        return this.doPost(parentPath, entity, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+    }
+
+    /**
+     * Wrapper method over {@link #importContent(String, String, String, int...)} for directly importing a json node
+     * @param parentPath path where the tree is created
+     * @param json json node with the desired structure
+     * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed
+     * @return the response
+     * @throws ClientException if something fails during the request/response cycle
+     */
+    public SlingHttpResponse importJson(String parentPath, JsonNode json, int... expectedStatus)
+            throws ClientException {
+        return importContent(parentPath, "json", json.toString(), expectedStatus);
+    }
+
+    /**
      * Get the UUID of a repository path
      *
      * @param path path in repository
diff --git a/src/main/java/org/apache/sling/testing/clients/email/package-info.java b/src/main/java/org/apache/sling/testing/clients/email/package-info.java
index 1789e2c..906392d 100644
--- a/src/main/java/org/apache/sling/testing/clients/email/package-info.java
+++ b/src/main/java/org/apache/sling/testing/clients/email/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@Version("1.1.0")
+@Version("1.2.0")
 package org.apache.sling.testing.clients.email;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/testing/clients/html/package-info.java b/src/main/java/org/apache/sling/testing/clients/html/package-info.java
index cd55c59..7c795ae 100644
--- a/src/main/java/org/apache/sling/testing/clients/html/package-info.java
+++ b/src/main/java/org/apache/sling/testing/clients/html/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@Version("2.2.0")
+@Version("2.3.0")
 package org.apache.sling.testing.clients.html;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/testing/clients/package-info.java b/src/main/java/org/apache/sling/testing/clients/package-info.java
index 9970e37..c3e99df 100644
--- a/src/main/java/org/apache/sling/testing/clients/package-info.java
+++ b/src/main/java/org/apache/sling/testing/clients/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@Version("1.4.0")
+@Version("1.5.0")
 package org.apache.sling.testing.clients;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/src/test/java/org/apache/sling/testing/clients/SlingClientImportContentTest.java b/src/test/java/org/apache/sling/testing/clients/SlingClientImportContentTest.java
new file mode 100644
index 0000000..7ad5938
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/clients/SlingClientImportContentTest.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.*;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.codehaus.jackson.node.JsonNodeFactory;
+import org.codehaus.jackson.node.ObjectNode;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_CREATED;
+
+public class SlingClientImportContentTest {
+    private static final Logger LOG = LoggerFactory.getLogger(SlingClientImportContentTest.class);
+
+    private static final String IMPORT_PATH = "/test/import/parent";
+    private static final String IMPORT_FILE_PATH = "/content/importfile";
+
+    private static final String IMPORT_FILE_CONTENT = "{\"nodefromfile\":{\"prop1\":\"val1\"}}";
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        @Override
+        protected void registerHandlers() throws IOException {
+            serverBootstrap.registerHandler(IMPORT_PATH, new HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException {
+                    List<NameValuePair> params = extractParameters(request);
+                    String operation = getParameter(":operation", params);
+                    String content = getParameter(":content", params);
+
+                    if (!"import".equals(operation)) {
+                        response.setStatusCode(SC_BAD_REQUEST);
+                        response.setEntity(new StringEntity("Unexpected operation: " + operation));
+                        return;
+                    }
+
+                    if (!"{\"something\":{\"prop1\":\"val1\"}}".equals(content)) {
+                        response.setStatusCode(SC_BAD_REQUEST);
+                        response.setEntity(new StringEntity("Unexpected content: " + content));
+                        return;
+                    }
+
+                    response.setStatusCode(SC_CREATED);
+                }
+            });
+
+            serverBootstrap.registerHandler(IMPORT_FILE_PATH, new HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException {
+                    LOG.debug("received: {}", request);
+                    if (request instanceof HttpEntityEnclosingRequest) {
+                        HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+                        String content = IOUtils.toString(entity.getContent(), UTF_8);
+                        LOG.debug("content: {}", content);
+
+                        if (!content.contains(":operation") || !content.contains("import")) {
+                            response.setStatusCode(SC_BAD_REQUEST);
+                            response.setEntity(new StringEntity("Operation not found"));
+                            return;
+                        } else if (!content.contains(IMPORT_FILE_CONTENT)) {
+                            response.setStatusCode(SC_BAD_REQUEST);
+                            response.setEntity(new StringEntity("File content not found"));
+                            return;
+                        }
+
+                        response.setStatusCode(SC_CREATED);
+                    } else {
+                        response.setStatusCode(SC_BAD_REQUEST);
+                        response.setEntity(new StringEntity("Request doesn't contain an entity"));
+                    }
+                }
+            });
+        }
+
+        private List<NameValuePair> extractParameters(HttpRequest request) {
+            if (request instanceof HttpEntityEnclosingRequest) {
+                HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+                try {
+                    return URLEncodedUtils.parse(entity);
+                } catch (IOException e) {
+                    LOG.error("Failed to parse entity", e);
+                }
+            }
+
+            return new ArrayList<>();
+        }
+
+        private String getParameter(String parameterName, List<NameValuePair> parameters) {
+            for (NameValuePair parameter : parameters) {
+                if (parameter.getName().equals(parameterName)) {
+                    return parameter.getValue();
+                }
+            }
+
+            return null;
+        }
+    };
+
+
+
+    private SlingClient client;
+
+    public SlingClientImportContentTest() throws ClientException {
+        client = new SlingClient(httpServer.getURI(), "user", "pass");
+        // to use with an already running instance
+        // client = new SlingClient(java.net.URI.create("http://localhost:8080"), "admin", "admin");
+    }
+
+    @Test
+    public void testImportContent() throws Exception {
+        client.importContent(IMPORT_PATH, "json", "{\"something\":{\"prop1\":\"val1\"}}");
+    }
+
+    @Test
+    public void testImportJson() throws Exception {
+        ObjectNode node = JsonNodeFactory.instance.objectNode();
+        ObjectNode props = JsonNodeFactory.instance.objectNode();
+        props.put("prop1", "val1");
+
+        node.put("something", props);
+        client.importJson(IMPORT_PATH, node);
+    }
+    @Test
+    public void testImportContentFile() throws Exception {
+        File tmp = File.createTempFile("import-json", null);
+        LOG.debug("created: " + tmp);
+        PrintWriter pw = new PrintWriter(tmp);
+        pw.write(IMPORT_FILE_CONTENT);
+        pw.close();
+
+        client.importContent(IMPORT_FILE_PATH, "json", tmp);
+    }
+}