From 072d40d66767b17f55174dba37838960e7513f17 Mon Sep 17 00:00:00 2001
From: alaskowski <alaskowski@ethz.ch>
Date: Thu, 9 Mar 2023 15:13:31 +0100
Subject: [PATCH] SSDM-13251: Added support for write, copy and move methods.
 Refactored tests

---
 .../ethz/sis/afsclient/client/AfsClient.java  |  41 ++++-
 .../sis/afsclient/client/AfsClientTest.java   |  50 +++++-
 .../sis/afsclient/client/DummyHttpServer.java |  30 ++--
 .../server/impl/ApiServerAdapter.java         | 160 ++++++++++--------
 .../ch/ethz/sis/afsserver/ApiClientTest.java  |  69 +++++++-
 .../core/AbstractPublicAPIWrapper.java        |  68 +++++---
 .../sis/afsserver/core/PublicApiTest.java     |  16 +-
 .../impl/APIServerAdapterWrapper.java         |  73 +++++---
 .../sis/afsserver/impl/APIServerWrapper.java  |   6 +-
 9 files changed, 362 insertions(+), 151 deletions(-)

diff --git a/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java b/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java
index 141c393222a..04643c68493 100644
--- a/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java
+++ b/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java
@@ -1,6 +1,9 @@
 package ch.ethz.sis.afsclient.client;
 
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
 import java.net.Authenticator;
 import java.net.PasswordAuthentication;
 import java.net.URI;
@@ -21,7 +24,6 @@ import ch.ethz.sis.afsjson.JsonObjectMapper;
 import ch.ethz.sis.afsjson.jackson.JacksonObjectMapper;
 import lombok.NonNull;
 
-
 public final class AfsClient implements PublicAPI
 {
 
@@ -81,8 +83,9 @@ public final class AfsClient implements PublicAPI
     public @NonNull String login(@NonNull final String userId, @NonNull final String password)
             throws Exception
     {
+        Map<String, String> credentials = Map.of("userId", userId, "password", password);
         String result = request("POST", "login", Map.of(),
-                (userId + ":" + password).getBytes());
+                jsonObjectMapper.writeValue(credentials));
         setSessionToken(result);
         return result;
     }
@@ -98,7 +101,8 @@ public final class AfsClient implements PublicAPI
     public @NonNull Boolean logout() throws Exception
     {
         validateSessionToken();
-        Boolean result = request("POST", "logout", Map.of(), getSessionToken().getBytes());
+        Boolean result = request("POST", "logout", Map.of(),
+                jsonObjectMapper.writeValue(Map.of("sessionToken", getSessionToken())));
         setSessionToken(null);
         return result;
     }
@@ -129,14 +133,25 @@ public final class AfsClient implements PublicAPI
             @NonNull final Long offset, final byte @NonNull [] data,
             final byte @NonNull [] md5Hash) throws Exception
     {
-        return null;
+        validateSessionToken();
+        Map<String, Object> parameters =
+                Map.of("owner", owner, "source", source,
+                        "offset", offset, "data", data, "md5Hash", md5Hash,
+                        "sessionToken", getSessionToken());
+
+        return request("POST", "write", Map.of(), jsonObjectMapper.writeValue(parameters));
     }
 
     @Override
     public @NonNull Boolean delete(@NonNull final String owner, @NonNull final String source)
             throws Exception
     {
-        return null;
+        validateSessionToken();
+        Map<String, Object> parameters =
+                Map.of("owner", owner, "source", source,
+                        "sessionToken", getSessionToken());
+
+        return request("DELETE", "delete", Map.of(), jsonObjectMapper.writeValue(parameters));
     }
 
     @Override
@@ -145,7 +160,13 @@ public final class AfsClient implements PublicAPI
             @NonNull final String target)
             throws Exception
     {
-        return null;
+        validateSessionToken();
+        Map<String, Object> parameters =
+                Map.of("sourceOwner", sourceOwner, "source", source,
+                        "targetOwner", targetOwner, "target", target,
+                        "sessionToken", getSessionToken());
+
+        return request("POST", "copy", Map.of(), jsonObjectMapper.writeValue(parameters));
     }
 
     @Override
@@ -154,7 +175,13 @@ public final class AfsClient implements PublicAPI
             @NonNull final String target)
             throws Exception
     {
-        return null;
+        validateSessionToken();
+        Map<String, Object> parameters =
+                Map.of("sourceOwner", sourceOwner, "source", source,
+                        "targetOwner", targetOwner, "target", target,
+                        "sessionToken", getSessionToken());
+
+        return request("POST", "move", Map.of(), jsonObjectMapper.writeValue(parameters));
     }
 
     @Override
diff --git a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java
index 56156c84813..654a2a554b5 100644
--- a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java
+++ b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java
@@ -42,6 +42,7 @@ public class AfsClientTest
         assertNotNull(token);
         assertEquals(token, afsClient.getSessionToken());
         assertEquals("POST", httpServer.getHttpExchange().getRequestMethod());
+        assertTrue(httpServer.getLastRequestBody().length > 0);
     }
 
     @Test
@@ -67,6 +68,7 @@ public class AfsClientTest
 
         assertTrue(result);
         assertEquals("GET", httpServer.getHttpExchange().getRequestMethod());
+        assertArrayEquals(httpServer.getLastRequestBody(), new byte[0]);
     }
 
     @Test
@@ -94,6 +96,7 @@ public class AfsClientTest
         assertTrue(result);
         assertNull(afsClient.getSessionToken());
         assertEquals("POST", httpServer.getHttpExchange().getRequestMethod());
+        assertTrue(httpServer.getLastRequestBody().length > 0);
     }
 
     @Test
@@ -105,6 +108,7 @@ public class AfsClientTest
         afsClient.list("", "", true);
 
         assertEquals("GET", httpServer.getHttpExchange().getRequestMethod());
+        assertArrayEquals(httpServer.getLastRequestBody(), new byte[0]);
     }
 
     @Test
@@ -115,30 +119,63 @@ public class AfsClientTest
         byte[] data = "ABCD".getBytes();
         httpServer.setNextResponse(data);
 
-        byte[] result = afsClient.read("admin", "/", 0L, 1000);
+        byte[] result = afsClient.read("", "", 0L, 1000);
 
         assertEquals("GET", httpServer.getHttpExchange().getRequestMethod());
         assertArrayEquals(data, result);
+        assertArrayEquals(httpServer.getLastRequestBody(), new byte[0]);
     }
 
     @Test
-    public void testWrite() throws Exception
+    public void write_methodIsPost() throws Exception
     {
+        login();
+
+        httpServer.setNextResponse("{\"result\": true}");
+        Boolean result = afsClient.write("", "", 0L, new byte[0], new byte[0]);
+
+        assertEquals("POST", httpServer.getHttpExchange().getRequestMethod());
+        assertTrue(result);
+        assertTrue(httpServer.getLastRequestBody().length > 0);
     }
 
     @Test
-    public void testDelete() throws Exception
+    public void delete_methodIsDelete() throws Exception
     {
+        login();
+
+        httpServer.setNextResponse("{\"result\": true}");
+        Boolean result = afsClient.delete("", "");
+
+        assertEquals("DELETE", httpServer.getHttpExchange().getRequestMethod());
+        assertTrue(result);
+        assertTrue(httpServer.getLastRequestBody().length > 0);
     }
 
     @Test
-    public void testCopy() throws Exception
+    public void copy_methodIsPost() throws Exception
     {
+        login();
+
+        httpServer.setNextResponse("{\"result\": true}");
+        Boolean result = afsClient.copy("", "", "", "");
+
+        assertEquals("POST", httpServer.getHttpExchange().getRequestMethod());
+        assertTrue(result);
+        assertTrue(httpServer.getLastRequestBody().length > 0);
     }
 
     @Test
-    public void testMove()
+    public void move_methodIsPost() throws Exception
     {
+        login();
+
+        httpServer.setNextResponse("{\"result\": true}");
+        Boolean result = afsClient.move("", "", "", "");
+
+        assertEquals("POST", httpServer.getHttpExchange().getRequestMethod());
+        assertTrue(result);
+        assertTrue(httpServer.getLastRequestBody().length > 0);
     }
 
     @Test
@@ -166,7 +203,8 @@ public class AfsClientTest
     {
     }
 
-    private void login() throws Exception {
+    private void login() throws Exception
+    {
         afsClient.login("test", "test");
     }
 
diff --git a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java
index dbc97199894..a1a06caea12 100644
--- a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java
+++ b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java
@@ -22,7 +22,6 @@ import java.net.HttpURLConnection;
 import java.net.InetSocketAddress;
 
 import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpHandler;
 import com.sun.net.httpserver.HttpServer;
 
 public final class DummyHttpServer
@@ -36,6 +35,9 @@ public final class DummyHttpServer
     private static final String DEFAULT_RESPONSE = "{\"result\": \"success\"}";
 
     private byte[] nextResponse = DEFAULT_RESPONSE.getBytes();
+
+    private byte[] lastRequestBody = null;
+
     private String nextResponseType = "application/json";
 
     private HttpExchange httpExchange;
@@ -45,18 +47,17 @@ public final class DummyHttpServer
         this.httpServerPort = httpServerPort;
         this.httpServerPath = httpServerPath;
         httpServer = HttpServer.create(new InetSocketAddress(httpServerPort), 0);
-        httpServer.createContext(httpServerPath, new HttpHandler()
+        httpServer.createContext(httpServerPath, exchange ->
         {
-            public void handle(HttpExchange exchange) throws IOException
-            {
-                byte[] response = nextResponse;
-                exchange.getResponseHeaders().set("content-type", nextResponseType);
-                exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length);
-
-                exchange.getResponseBody().write(response);
-                exchange.close();
-                httpExchange = exchange;
-            }
+            byte[] response = nextResponse;
+            exchange.getResponseHeaders().set("content-type", nextResponseType);
+            exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length);
+
+            exchange.getResponseBody().write(response);
+            lastRequestBody = exchange.getRequestBody().readAllBytes();
+
+            exchange.close();
+            httpExchange = exchange;
         });
     }
 
@@ -82,6 +83,11 @@ public final class DummyHttpServer
         this.nextResponseType = "application/octet-stream";
     }
 
+    public byte[] getLastRequestBody()
+    {
+        return lastRequestBody;
+    }
+
     public HttpExchange getHttpExchange()
     {
         return httpExchange;
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
index ae64cc19675..d0a5c3ad106 100644
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
@@ -15,19 +15,17 @@
  */
 package ch.ethz.sis.afsserver.server.impl;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import ch.ethz.sis.afsserver.exception.HTTPExceptions;
 import ch.ethz.sis.afsserver.http.*;
 import ch.ethz.sis.afsserver.server.*;
 import ch.ethz.sis.afsserver.server.performance.Event;
 import ch.ethz.sis.afsserver.server.performance.PerformanceAuditor;
-import ch.ethz.sis.shared.io.IOUtils;
 import ch.ethz.sis.afsjson.JsonObjectMapper;
 import ch.ethz.sis.shared.log.LogManager;
 import ch.ethz.sis.shared.log.Logger;
 import io.netty.handler.codec.http.HttpMethod;
 
+import java.io.ByteArrayInputStream;
 import java.util.*;
 
 /*
@@ -92,84 +90,107 @@ public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler
             String interactiveSessionKey = null;
             String transactionManagerKey = null;
             Map<String, Object> methodParameters = new HashMap<>();
-            for (Map.Entry<String, List<String>> entry : uriParameters.entrySet())
+            if (HttpMethod.GET.equals(httpMethod))
             {
-                String value = null;
-                if (entry.getValue() != null)
+                for (Map.Entry<String, List<String>> entry : uriParameters.entrySet())
                 {
-                    if (entry.getValue().size() == 1)
+                    String value = null;
+                    if (entry.getValue() != null)
+                    {
+                        if (entry.getValue().size() == 1)
+                        {
+                            value = entry.getValue().get(0);
+                        } else if (entry.getValue().size() > 1)
+                        {
+                            return getHTTPResponse(new ApiResponse("1", null,
+                                    HTTPExceptions.INVALID_PARAMETERS.getCause()));
+                        }
+                    }
+
+                    try
                     {
-                        value = entry.getValue().get(0);
-                    } else if (entry.getValue().size() > 1)
+                        switch (entry.getKey())
+                        {
+                            case "method":
+                                method = value;
+                                if (!isValidMethod(httpMethod, method))
+                                {
+                                    return getHTTPResponse(new ApiResponse("1", null,
+                                            HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
+                                }
+                                break;
+                            case "sessionToken":
+                                sessionToken = value;
+                                break;
+                            case "interactiveSessionKey":
+                                interactiveSessionKey = value;
+                                break;
+                            case "transactionManagerKey":
+                                transactionManagerKey = value;
+                                break;
+                            case "transactionId":
+                                methodParameters.put(entry.getKey(), UUID.fromString(value));
+                                break;
+                            case "recursively":
+                                methodParameters.put(entry.getKey(), Boolean.valueOf(value));
+                                break;
+                            case "offset":
+                                methodParameters.put(entry.getKey(), Long.valueOf(value));
+                                break;
+                            case "limit":
+                                methodParameters.put(entry.getKey(), Integer.valueOf(value));
+                                break;
+                            default:
+                                methodParameters.put(entry.getKey(), value);
+                                break;
+                        }
+                    } catch (Exception e)
                     {
+                        logger.catching(e);
                         return getHTTPResponse(new ApiResponse("1", null,
-                                HTTPExceptions.INVALID_PARAMETERS.getCause()));
+                                HTTPExceptions.INVALID_PARAMETERS.getCause(
+                                        e.getClass().getSimpleName(),
+                                        e.getMessage())));
                     }
                 }
-
-                try
+            } else if (HttpMethod.POST.equals(httpMethod) || HttpMethod.DELETE.equals(httpMethod))
+            {
+                if (!uriParameters.containsKey("method"))
+                {
+                    return getHTTPResponse(new ApiResponse("1", null,
+                            HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
+                } else
                 {
-                    switch (entry.getKey())
+                    List<String> methodParam = uriParameters.get("method");
+                    if (methodParam == null || methodParam.size() != 1)
                     {
-                        case "method":
-                            method = value;
-                            if (!isValidMethod(httpMethod, method))
-                            {
-                                return getHTTPResponse(new ApiResponse("1", null,
-                                        HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
-                            }
-                            break;
-                        case "sessionToken":
-                            sessionToken = value;
-                            break;
-                        case "interactiveSessionKey":
-                            interactiveSessionKey = value;
-                            break;
-                        case "transactionManagerKey":
-                            transactionManagerKey = value;
-                            break;
-                        case "transactionId":
-                            methodParameters.put(entry.getKey(), UUID.fromString(value));
-                            break;
-                        case "recursively":
-                            methodParameters.put(entry.getKey(), Boolean.valueOf(value));
-                            break;
-                        case "offset":
-                            methodParameters.put(entry.getKey(), Long.valueOf(value));
-                            break;
-                        case "limit":
-                            methodParameters.put(entry.getKey(), Integer.valueOf(value));
-                            break;
-                        case "md5Hash":
-                            methodParameters.put(entry.getKey(), IOUtils.decodeBase64(value));
-                            break;
-                        default:
-                            methodParameters.put(entry.getKey(), value);
-                            break;
+                        return getHTTPResponse(new ApiResponse("1", null,
+                                HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
                     }
-                } catch (Exception e)
+                    method = methodParam.get(0);
+                }
+
+                if (!isValidMethod(httpMethod, method))
                 {
-                    logger.catching(e);
                     return getHTTPResponse(new ApiResponse("1", null,
-                            HTTPExceptions.INVALID_PARAMETERS.getCause(e.getClass().getSimpleName(),
-                                    e.getMessage())));
+                            HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
                 }
-            }
+                Map<String, Object> bodyParameterMap = readBody(requestBody, HashMap.class);
 
-            // Process body parameters
-            switch (method) {
-                case "write":
-                    methodParameters.put("data", requestBody);
-                    break;
-                case "login":
-                    // userId : password
-                    String[] credentials = new String(requestBody, UTF_8).split(":");
-                    methodParameters.put("userId", credentials[0]);
-                    methodParameters.put("password", credentials[1]);
-                    break;
-                case "logout":
-                    sessionToken = new String(requestBody, UTF_8);
-                    break;
+                for (Map.Entry<String, Object> entry : bodyParameterMap.entrySet())
+                {
+                    if (entry.getKey().equals("sessionToken"))
+                    {
+                        sessionToken = (String) entry.getValue();
+                    } else
+                    {
+                        methodParameters.put(entry.getKey(), entry.getValue());
+                    }
+                }
+            } else
+            {
+                return getHTTPResponse(new ApiResponse("1", null,
+                        HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
             }
 
             ApiRequest apiRequest = new ApiRequest("1", method, methodParameters, sessionToken,
@@ -213,6 +234,11 @@ public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler
         return null; // This should never happen, it would mean an error writing the Unknown error happened.
     }
 
+    private <T> T readBody(byte[] requestBody, Class<T> valueType) throws Exception
+    {
+        return (T) jsonObjectMapper.readValue(new ByteArrayInputStream(requestBody), valueType);
+    }
+
     private HttpResponse getHTTPResponse(Response response) throws Exception
     {
         boolean error = response.getError() != null;
diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java
index 1d3d61be291..3d54804e03b 100644
--- a/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java
+++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java
@@ -23,13 +23,11 @@ import static org.junit.Assert.*;
 
 import java.io.IOException;
 import java.net.URI;
-import java.nio.file.Paths;
 import java.util.List;
 import java.util.UUID;
 
 import ch.ethz.sis.afsapi.dto.File;
 import ch.ethz.sis.shared.io.IOUtils;
-import org.apache.commons.io.FileUtils;
 import org.junit.*;
 
 import ch.ethz.sis.afs.manager.TransactionConnection;
@@ -38,7 +36,6 @@ import ch.ethz.sis.afsserver.server.Server;
 import ch.ethz.sis.afsserver.server.observer.impl.DummyServerObserver;
 import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
 import ch.ethz.sis.shared.startup.Configuration;
-import org.junit.rules.TemporaryFolder;
 
 public final class ApiClientTest
 {
@@ -55,11 +52,13 @@ public final class ApiClientTest
     public static final String FILE_A = "A.txt";
 
     public static final byte[] DATA = "ABCD".getBytes();
-
     public static final String FILE_B = "B.txt";
 
     public static String owner = UUID.randomUUID().toString();
 
+    private String testDataRoot;
+
+
     @BeforeClass
     public static void classSetUp() throws Exception
     {
@@ -84,7 +83,7 @@ public final class ApiClientTest
     @Before
     public void setUp() throws Exception
     {
-        String testDataRoot = IOUtils.getPath(storageRoot, owner.toString());
+        testDataRoot = IOUtils.getPath(storageRoot, owner.toString());
         IOUtils.createDirectories(testDataRoot);
         String testDataFile = IOUtils.getPath(testDataRoot, FILE_A);
         IOUtils.createFile(testDataFile);
@@ -171,6 +170,66 @@ public final class ApiClientTest
         assertArrayEquals(DATA, bytes);
     }
 
+    @Test
+    public void write_zeroOffset_createsFile() throws Exception {
+        login();
+
+        Boolean result = afsClient.write(owner, FILE_B, 0L, DATA, IOUtils.getMD5(DATA));
+        assertTrue(result);
+
+        byte[] testDataFile = IOUtils.readFully(IOUtils.getPath(testDataRoot, FILE_B));
+        assertArrayEquals(DATA, testDataFile);
+    }
+
+    @Test
+    public void write_nonZeroOffset_createsFile() throws Exception {
+        login();
+
+        Long offset = 65L;
+        Boolean result = afsClient.write(owner, FILE_B, offset, DATA, IOUtils.getMD5(DATA));
+        assertTrue(result);
+
+        byte[] testDataFile = IOUtils.readFully(IOUtils.getPath(testDataRoot, FILE_A));
+        assertArrayEquals(DATA, testDataFile);
+    }
+
+    @Test
+    public void delete_fileIsGone() throws Exception {
+        login();
+
+        Boolean deleted = afsClient.delete(owner, FILE_A);
+        assertTrue(deleted);
+
+        List<ch.ethz.sis.afs.api.dto.File> list =  IOUtils.list(testDataRoot, true);
+        assertEquals(0, list.size());
+    }
+
+    @Test
+    public void copy_newFileIsCreated() throws Exception {
+        login();
+
+        Boolean result = afsClient.copy(owner, FILE_A, owner, FILE_B);
+        assertTrue(result);
+
+        byte[] testDataFile = IOUtils.readFully(IOUtils.getPath(testDataRoot, FILE_B));
+        assertArrayEquals(DATA, testDataFile);
+    }
+
+    @Test
+    public void move_fileIsRenamed() throws Exception {
+        login();
+
+        Boolean result = afsClient.move(owner, FILE_A, owner, FILE_B);
+        assertTrue(result);
+
+        List<ch.ethz.sis.afs.api.dto.File> list =  IOUtils.list(testDataRoot, true);
+        assertEquals(1, list.size());
+        assertEquals(FILE_B, list.get(0).getName());
+
+        byte[] testDataFile = IOUtils.readFully(IOUtils.getPath(testDataRoot, FILE_B));
+        assertArrayEquals(DATA, testDataFile);
+    }
+
 
 
     private String login() throws Exception
diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java
index 68d8338a2d4..27853db9e03 100644
--- a/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java
+++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java
@@ -19,113 +19,137 @@ import ch.ethz.sis.afsapi.dto.File;
 import ch.ethz.sis.afsapi.api.PublicAPI;
 import lombok.NonNull;
 
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-public abstract class AbstractPublicAPIWrapper implements PublicAPI {
+public abstract class AbstractPublicAPIWrapper implements PublicAPI
+{
 
-    public abstract <E> E process(String method, Map<String, Object> methodParameters);
+    public abstract <E> E process(String method, Map<String, Object> queryParams,
+            Map<String, Object> bodyParams);
 
     @Override
-    public List<File> list(@NonNull String owner, @NonNull String source, @NonNull Boolean recursively) throws Exception {
+    public List<File> list(@NonNull String owner, @NonNull String source,
+            @NonNull Boolean recursively) throws Exception
+    {
         Map<String, Object> args = Map.of(
                 "owner", owner,
                 "source", source,
                 "recursively", recursively);
-        return process("list", args);
+        return process("list", args, Map.of());
     }
 
     @Override
-    public byte[] read(@NonNull String owner, @NonNull String source, @NonNull Long offset, @NonNull Integer limit) throws Exception {
+    public byte[] read(@NonNull String owner, @NonNull String source, @NonNull Long offset,
+            @NonNull Integer limit) throws Exception
+    {
         Map<String, Object> args = Map.of(
                 "owner", owner,
                 "source", source,
                 "offset", offset,
                 "limit", limit);
-        return process("read", args);
+        return process("read", args, Map.of());
     }
 
     @Override
-    public Boolean write(@NonNull String owner, @NonNull String source, @NonNull Long offset, @NonNull byte[] data, @NonNull byte[] md5Hash) throws Exception {
+    public Boolean write(@NonNull String owner, @NonNull String source, @NonNull Long offset,
+            @NonNull byte[] data, @NonNull byte[] md5Hash) throws Exception
+    {
         Map<String, Object> args = Map.of(
                 "owner", owner,
                 "source", source,
                 "offset", offset,
                 "data", data,
                 "md5Hash", md5Hash);
-        return process("write", args);
+        return process("write", Map.of(), args);
     }
 
     @Override
-    public Boolean delete(@NonNull String owner, @NonNull String source) throws Exception {
+    public Boolean delete(@NonNull String owner, @NonNull String source) throws Exception
+    {
         Map<String, Object> args = Map.of(
                 "owner", owner,
                 "source", source);
-        return process("delete", args);
+        return process("delete", Map.of(), args);
     }
 
     @Override
-    public Boolean copy(@NonNull String sourceOwner, @NonNull String source, @NonNull String targetOwner, @NonNull String target) throws Exception {
+    public Boolean copy(@NonNull String sourceOwner, @NonNull String source,
+            @NonNull String targetOwner, @NonNull String target) throws Exception
+    {
         Map<String, Object> args = Map.of(
                 "sourceOwner", sourceOwner,
                 "source", source,
                 "targetOwner", targetOwner,
                 "target", target);
-        return process("copy", args);
+        return process("copy", Map.of(), args);
     }
 
     @Override
-    public Boolean move(@NonNull String sourceOwner, @NonNull String source, @NonNull String targetOwner, @NonNull String target) throws Exception {
+    public Boolean move(@NonNull String sourceOwner, @NonNull String source,
+            @NonNull String targetOwner, @NonNull String target) throws Exception
+    {
         Map<String, Object> args = Map.of(
                 "sourceOwner", sourceOwner,
                 "source", source,
                 "targetOwner", targetOwner,
                 "target", target);
-        return process("move", args);
+        return process("move", Map.of(), args);
     }
 
     @Override
-    public void begin(UUID transactionId) throws Exception {
+    public void begin(UUID transactionId) throws Exception
+    {
         //TODO: Unused
     }
 
     @Override
-    public Boolean prepare() throws Exception {
+    public Boolean prepare() throws Exception
+    {
         //TODO: Unused
         return true;
     }
 
     @Override
-    public void commit() throws Exception {
+    public void commit() throws Exception
+    {
         //TODO: Unused
     }
 
     @Override
-    public void rollback() throws Exception {
+    public void rollback() throws Exception
+    {
         //TODO: Unused
     }
 
     @Override
-    public List<UUID> recover() throws Exception {
+    public List<UUID> recover() throws Exception
+    {
         //TODO: Unused
         return null;
     }
 
     @Override
-    public String login(String userId, String password) throws Exception {
+    public String login(String userId, String password) throws Exception
+    {
         //TODO: Unused
         return null;
     }
 
     @Override
-    public Boolean isSessionValid() throws Exception {
+    public Boolean isSessionValid() throws Exception
+    {
         //TODO: Unused
         return null;
     }
 
     @Override
-    public Boolean logout() throws Exception {
+    public Boolean logout() throws Exception
+    {
         //TODO: Unused
         return null;
     }
diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java
index 4af73090c05..186b8a1e289 100644
--- a/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java
+++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java
@@ -85,29 +85,29 @@ public abstract class PublicApiTest extends AbstractTest
     @Test
     public void read() throws Exception
     {
-        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_A, 0L, DATA.length);
+        byte[] bytes = getPublicAPI().read(owner, FILE_A, 0L, DATA.length);
         assertArrayEquals(DATA, bytes);
     }
 
     @Test(expected = RuntimeException.class)
     public void read_big_failure() throws Exception
     {
-        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_A, 0L, Integer.MAX_VALUE);
+        byte[] bytes = getPublicAPI().read(owner, FILE_A, 0L, Integer.MAX_VALUE);
         assertArrayEquals(DATA, bytes);
     }
 
     @Test
     public void write() throws Exception
     {
-        getPublicAPI().write(owner, "/" + FILE_B, 0L, DATA, IOUtils.getMD5(DATA));
-        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_B, 0L, DATA.length);
+        getPublicAPI().write(owner,  FILE_B, 0L, DATA, IOUtils.getMD5(DATA));
+        byte[] bytes = getPublicAPI().read(owner, FILE_B, 0L, DATA.length);
         assertArrayEquals(DATA, bytes);
     }
 
     @Test
     public void delete() throws Exception
     {
-        Boolean deleted = getPublicAPI().delete(owner, "/" + FILE_A);
+        Boolean deleted = getPublicAPI().delete(owner, FILE_A);
         assertTrue(deleted);
         List<File> list = getPublicAPI().list(owner, ROOT, Boolean.TRUE);
         assertEquals(0, list.size());
@@ -116,15 +116,15 @@ public abstract class PublicApiTest extends AbstractTest
     @Test
     public void copy() throws Exception
     {
-        getPublicAPI().copy(owner, "/" + FILE_A, owner, "/" + FILE_B);
-        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_B, 0L, DATA.length);
+        getPublicAPI().copy(owner, FILE_A, owner, FILE_B);
+        byte[] bytes = getPublicAPI().read(owner, FILE_B, 0L, DATA.length);
         assertArrayEquals(DATA, bytes);
     }
 
     @Test
     public void move() throws Exception
     {
-        getPublicAPI().move(owner, "/" + FILE_A, owner, "/" + FILE_B);
+        getPublicAPI().move(owner, FILE_A, owner, FILE_B);
         List<File> list = getPublicAPI().list(owner, ROOT, Boolean.TRUE);
         assertEquals(1, list.size());
         assertEquals(FILE_B, list.get(0).getName());
diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapper.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapper.java
index 6c6ff9644b4..b4fbd223a43 100644
--- a/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapper.java
+++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapper.java
@@ -15,7 +15,6 @@
  */
 package ch.ethz.sis.afsserver.impl;
 
-
 import ch.ethz.sis.afsserver.core.AbstractPublicAPIWrapper;
 import ch.ethz.sis.afsserver.http.HttpResponse;
 import ch.ethz.sis.afsserver.server.impl.ApiRequest;
@@ -35,54 +34,82 @@ import java.util.*;
 import static ch.ethz.sis.afsserver.http.HttpResponse.CONTENT_TYPE_BINARY_DATA;
 import static ch.ethz.sis.afsserver.http.HttpResponse.CONTENT_TYPE_JSON;
 
-public class APIServerAdapterWrapper extends AbstractPublicAPIWrapper {
+public class APIServerAdapterWrapper extends AbstractPublicAPIWrapper
+{
 
     private static final Logger logger = LogManager.getLogger(APIServerAdapterWrapper.class);
 
     private ApiServerAdapter apiServerAdapter;
+
     private JsonObjectMapper jsonObjectMapper;
 
-    public APIServerAdapterWrapper(ApiServerAdapter apiServerAdapter, JsonObjectMapper jsonObjectMapper) {
+    public APIServerAdapterWrapper(ApiServerAdapter apiServerAdapter,
+            JsonObjectMapper jsonObjectMapper)
+    {
         this.apiServerAdapter = apiServerAdapter;
         this.jsonObjectMapper = jsonObjectMapper;
     }
 
-    public Map<String, List<String>> getURIParameters(Map<String, Object> args) {
+    public Map<String, List<String>> getURIParameters(Map<String, Object> args)
+    {
         Map<String, List<String>> result = new HashMap<>(args.size());
-        for (Map.Entry<String, Object> entry:args.entrySet()) {
-            if (entry.getKey().equals("data") && entry.getValue() instanceof byte[]) {
+        for (Map.Entry<String, Object> entry : args.entrySet())
+        {
+            if (entry.getKey().equals("data") && entry.getValue() instanceof byte[])
+            {
                 continue; // Skip
-            } else if(entry.getValue() instanceof byte[]) {
-                result.put(entry.getKey(), List.of(IOUtils.encodeBase64((byte[]) entry.getValue())));
-            } else {
+            } else if (entry.getValue() instanceof byte[])
+            {
+                result.put(entry.getKey(),
+                        List.of(IOUtils.encodeBase64((byte[]) entry.getValue())));
+            } else
+            {
                 result.put(entry.getKey(), List.of(String.valueOf(entry.getValue())));
             }
         }
         return result;
     }
 
-    public <E> E process(String apiMethod, Map<String, Object> args) {
-        try {
+    public <E> E process(String apiMethod, Map<String, Object> queryParams,
+            Map<String, Object> bodyParams)
+    {
+        try
+        {
             HttpMethod httpMethod = ApiServerAdapter.getHttpMethod(apiMethod);
-
-            Map<String, List<String>> uriParameters = getURIParameters(args);
-            uriParameters.put("method", List.of(apiMethod));
-            uriParameters.put("sessionToken", List.of(UUID.randomUUID().toString()));
-
+            Map<String, List<String>> uriParameters = new HashMap<>();
             byte[] requestBody = null;
-            if (apiMethod.equals("write")) {
-                requestBody = (byte[]) args.get("data");
+
+            if (HttpMethod.GET.equals(httpMethod))
+            {
+                uriParameters = getURIParameters(queryParams);
+                uriParameters.put("sessionToken", List.of(UUID.randomUUID().toString()));
+            } else if (HttpMethod.POST.equals(httpMethod) || HttpMethod.DELETE.equals(httpMethod))
+            {
+                Map<String, Object> fullBodyParams = new HashMap<>();
+                fullBodyParams.putAll(bodyParams);
+                fullBodyParams.put("sessionToken", UUID.randomUUID().toString());
+                requestBody = jsonObjectMapper.writeValue(fullBodyParams);
+            } else
+            {
+                throw new IllegalArgumentException("Not supported HTTP method type!");
             }
 
-            HttpResponse response = apiServerAdapter.process(httpMethod, uriParameters, requestBody);
-            switch (response.getContentType()) {
+            uriParameters.put("method", List.of(apiMethod));
+
+            HttpResponse response =
+                    apiServerAdapter.process(httpMethod, uriParameters, requestBody);
+            switch (response.getContentType())
+            {
                 case CONTENT_TYPE_BINARY_DATA:
                     return (E) response.getBody();
                 case CONTENT_TYPE_JSON:
-                    ApiResponse apiResponse = jsonObjectMapper.readValue(new ByteArrayInputStream(response.getBody()), ApiResponse.class);
-                    return  (E) apiResponse.getResult();
+                    ApiResponse apiResponse =
+                            jsonObjectMapper.readValue(new ByteArrayInputStream(response.getBody()),
+                                    ApiResponse.class);
+                    return (E) apiResponse.getResult();
             }
-        } catch (Throwable throwable) {
+        } catch (Throwable throwable)
+        {
             throw new RuntimeException(throwable);
         }
         throw new RuntimeException("This line should be unreachable");
diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapper.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapper.java
index 15d5f4eb397..4f0b7cbc308 100644
--- a/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapper.java
+++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapper.java
@@ -25,6 +25,7 @@ import ch.ethz.sis.afsserver.server.performance.PerformanceAuditor;
 import ch.ethz.sis.shared.log.LogManager;
 import ch.ethz.sis.shared.log.Logger;
 
+import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
@@ -40,9 +41,12 @@ public class APIServerWrapper extends AbstractPublicAPIWrapper {
         this.apiResponseBuilder = new ApiResponseBuilder();
     }
 
-    public <E> E process(String method, Map<String, Object> args) {
+    public <E> E process(String method, Map<String, Object> queryParams, Map<String, Object> bodyParams) {
         PerformanceAuditor performanceAuditor = new PerformanceAuditor();
         // Random Session token just works for tests with dummy authentication
+        Map<String, Object> args = new HashMap<>();
+        args.putAll(queryParams);
+        args.putAll(bodyParams);
         ApiRequest request = new ApiRequest("test", method, args, UUID.randomUUID().toString(), null, null);
 
         try {
-- 
GitLab