com.pcloud.sdk.internal.RealApiClient.java Source code

Java tutorial

Introduction

Here is the source code for com.pcloud.sdk.internal.RealApiClient.java

Source

/*
 * Copyright (c) 2017 pCloud AG
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.pcloud.sdk.internal;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.stream.JsonReader;
import com.pcloud.sdk.*;
import com.pcloud.sdk.Authenticator;
import com.pcloud.sdk.Call;
import com.pcloud.sdk.internal.networking.*;
import com.pcloud.sdk.internal.networking.ApiResponse;
import com.pcloud.sdk.internal.networking.GetFileResponse;
import com.pcloud.sdk.internal.networking.GetFolderResponse;
import com.pcloud.sdk.internal.networking.UploadFilesResponse;

import com.pcloud.sdk.internal.networking.serialization.DateTypeAdapter;
import com.pcloud.sdk.internal.networking.serialization.UnmodifiableListTypeFactory;
import okhttp3.HttpUrl;
import okhttp3.*;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

import static com.pcloud.sdk.internal.FileIdUtils.*;
import static com.pcloud.sdk.internal.IOUtils.closeQuietly;

class RealApiClient implements ApiClient {

    private long progressCallbackThresholdBytes;
    private Authenticator authenticator;
    private Gson gson;
    private OkHttpClient httpClient;
    private Executor callbackExecutor;

    private static final HttpUrl API_BASE_URL = HttpUrl.parse("https://api.pcloud.com");

    RealApiClient() {
        this(new RealApiServiceBuilder());
    }

    RealApiClient(RealApiServiceBuilder builder) {
        Map<String, String> globalParams = new TreeMap<>();
        globalParams.put("timeformat", String.valueOf("timestamp"));
        String userAgent = String.format(Locale.US, "pCloud SDK Java %s", Version.NAME);
        OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
                .readTimeout(builder.readTimeoutMs(), TimeUnit.MILLISECONDS)
                .writeTimeout(builder.writeTimeoutMs(), TimeUnit.MILLISECONDS)
                .connectTimeout(builder.connectTimeoutMs(), TimeUnit.MILLISECONDS)
                .protocols(Collections.singletonList(Protocol.HTTP_1_1))
                .addInterceptor(new GlobalRequestInterceptor(userAgent, globalParams));

        if (builder.dispatcher() != null) {
            httpClientBuilder.dispatcher(builder.dispatcher());
        }

        if (builder.connectionPool() != null) {
            httpClientBuilder.connectionPool(builder.connectionPool());
        }

        if (builder.cache() != null) {
            httpClientBuilder.cache(builder.cache());
        }

        httpClientBuilder.authenticator(okhttp3.Authenticator.NONE);
        this.authenticator = builder.authenticator();
        if (authenticator != null) {
            httpClientBuilder.addInterceptor((RealAuthenticator) builder.authenticator());
        }

        this.httpClient = httpClientBuilder.build();
        this.callbackExecutor = builder.callbackExecutor();
        this.progressCallbackThresholdBytes = builder.progressCallbackThresholdBytes();
        this.gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation()
                .registerTypeAdapterFactory(new RealRemoteEntry.TypeAdapterFactory())
                .registerTypeAdapterFactory(new UnmodifiableListTypeFactory())
                .registerTypeAdapter(RemoteEntry.class, new RealRemoteEntry.FileEntryDeserializer())
                .registerTypeAdapter(Date.class, new DateTypeAdapter())
                .registerTypeAdapter(RealRemoteFile.class, new RealRemoteFile.InstanceCreator(this))
                .registerTypeAdapter(RealRemoteFolder.class, new RealRemoteFolder.InstanceCreator(this)).create();
    }

    @Override
    public Call<RemoteFolder> listFolder(long folderId) {
        return listFolder(folderId, false);
    }

    @Override
    public Call<RemoteFolder> listFolder(long folderId, boolean recursively) {
        return newCall(createListFolderRequest(folderId, recursively), new ResponseAdapter<RemoteFolder>() {
            @Override
            public RemoteFolder adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFolderResponse.class).getFolder();
            }
        });
    }

    @Override
    public Call<RemoteFile> createFile(RemoteFolder folder, String filename, DataSource data) {
        return createFile(folder, filename, data, null, null, UploadOptions.DEFAULT);
    }

    @Override
    public Call<RemoteFile> createFile(RemoteFolder folder, String filename, DataSource data,
            UploadOptions uploadOptions) {
        return createFile(folder, filename, data, null, null, uploadOptions);
    }

    @Override
    public Call<RemoteFile> createFile(RemoteFolder folder, String filename, DataSource data, Date modifiedDate,
            ProgressListener listener) {
        return createFile(folder, filename, data, modifiedDate, listener, UploadOptions.DEFAULT);
    }

    @Override
    public Call<RemoteFile> createFile(RemoteFolder folder, String filename, DataSource data, Date modifiedDate,
            ProgressListener listener, UploadOptions uploadOptions) {
        if (folder == null) {
            throw new IllegalArgumentException("Folder argument cannot be null.");
        }
        return createFile(folder.folderId(), filename, data, modifiedDate, listener, uploadOptions);
    }

    @Override
    public Call<RemoteFile> createFile(long folderId, String filename, DataSource data) {
        return createFile(folderId, filename, data, null, null, UploadOptions.DEFAULT);
    }

    @Override
    public Call<RemoteFile> createFile(long folderId, String filename, DataSource data,
            UploadOptions uploadOptions) {
        return createFile(folderId, filename, data, null, null, uploadOptions);
    }

    @Override
    public Call<RemoteFile> createFile(long folderId, String filename, DataSource data, Date modifiedDate,
            ProgressListener listener) {
        return createFile(folderId, filename, data, modifiedDate, listener, UploadOptions.DEFAULT);
    }

    @Override
    public Call<RemoteFile> createFile(long folderId, String filename, final DataSource data, Date modifiedDate,
            final ProgressListener listener, final UploadOptions uploadOptions) {
        if (filename == null) {
            throw new IllegalArgumentException("Filename cannot be null.");
        }
        if (data == null) {
            throw new IllegalArgumentException("File data cannot be null.");
        }

        if (uploadOptions == null) {
            throw new IllegalArgumentException("Upload options cannot be null.");
        }

        RequestBody dataBody = new RequestBody() {
            @Override
            public MediaType contentType() {
                return MediaType.parse("multipart/form-data");
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                if (listener != null) {
                    ProgressListener realListener = listener;
                    if (callbackExecutor != null) {
                        realListener = new ExecutorProgressListener(listener, callbackExecutor);
                    }

                    sink = Okio.buffer(new ProgressCountingSink(sink, data.contentLength(), realListener,
                            progressCallbackThresholdBytes));
                }
                data.writeTo(sink);
                sink.flush();
            }

            @Override
            public long contentLength() throws IOException {
                return data.contentLength();
            }
        };

        RequestBody compositeBody = new MultipartBody.Builder("--").setType(MultipartBody.FORM)
                .addFormDataPart("file", filename, dataBody).build();

        HttpUrl.Builder urlBuilder = API_BASE_URL.newBuilder().addPathSegment("uploadfile")
                .addQueryParameter("folderid", String.valueOf(folderId))
                .addQueryParameter("renameifexists", String.valueOf(uploadOptions.overrideFile() ? 0 : 1))
                .addQueryParameter("nopartial", String.valueOf(uploadOptions.partialUpload() ? 0 : 1));

        if (modifiedDate != null) {
            urlBuilder.addQueryParameter("mtime",
                    String.valueOf(TimeUnit.MILLISECONDS.toSeconds(modifiedDate.getTime())));
        }

        Request uploadRequest = new Request.Builder().url(urlBuilder.build()).method("POST", compositeBody).build();

        return newCall(uploadRequest, new ResponseAdapter<RemoteFile>() {
            @Override
            public RemoteFile adapt(Response response) throws IOException, ApiError {
                UploadFilesResponse body = getAsApiResponse(response, UploadFilesResponse.class);
                if (!body.getUploadedFiles().isEmpty()) {
                    return body.getUploadedFiles().get(0);
                } else {
                    throw new IOException("API uploaded file but did not return remote file data.");
                }
            }
        });
    }

    @Override
    public Call<Boolean> deleteFile(RemoteFile file) {
        if (file == null) {
            throw new IllegalArgumentException("File argument cannot be null.");
        }
        return deleteFile(file.fileId());
    }

    @Override
    public Call<Boolean> deleteFile(long fileId) {
        Request request = new Request.Builder().url(API_BASE_URL.newBuilder().addPathSegment("deletefile").build())
                .get().post(new FormBody.Builder().add("fileid", String.valueOf(fileId)).build()).build();
        return newCall(request, new ResponseAdapter<Boolean>() {
            @Override
            public Boolean adapt(Response response) throws IOException, ApiError {
                GetFileResponse body = deserializeResponseBody(response, GetFileResponse.class);
                return body.isSuccessful() && body.getFile() != null;
            }
        });
    }

    @Override
    public Call<FileLink> createFileLink(RemoteFile file, DownloadOptions options) {
        if (file == null) {
            throw new IllegalArgumentException("File argument cannot be null.");
        }
        return createFileLink(file.fileId(), options);
    }

    @Override
    public Call<FileLink> createFileLink(long fileId, DownloadOptions options) {
        if (options == null) {
            throw new IllegalArgumentException("DownloadOptions parameter cannot be null.");
        }

        Request request = newDownloadLinkRequest(fileId, options);

        return newCall(request, new ResponseAdapter<FileLink>() {
            @Override
            public FileLink adapt(Response response) throws IOException, ApiError {
                return getAsFileLink(response);
            }
        });

    }

    private FileLink getAsFileLink(Response response) throws IOException, ApiError {
        GetLinkResponse body = getAsApiResponse(response, GetLinkResponse.class);
        List<URL> downloadUrls = new ArrayList<>(body.getHosts().size());
        for (String host : body.getHosts()) {
            downloadUrls.add(new URL("https", host, body.getPath()));
        }

        return new RealFileLink(RealApiClient.this, body.getExpires(), downloadUrls);
    }

    private Request newDownloadLinkRequest(long fileId, DownloadOptions options) {
        HttpUrl.Builder urlBuilder = API_BASE_URL.newBuilder().addPathSegment("getfilelink")
                .addQueryParameter("fileid", String.valueOf(fileId));

        if (options.forceDownload()) {
            urlBuilder.addQueryParameter("forcedownload", String.valueOf(1));
        }

        if (options.skipFilename()) {
            urlBuilder.addQueryParameter("skipfilename", String.valueOf(1));
        }

        if (options.contentType() != null) {
            MediaType mediaType = MediaType.parse(options.contentType());
            if (mediaType == null) {
                throw new IllegalArgumentException(
                        "Invalid or not well-formatted content type DownloadOptions argument");
            }
            urlBuilder.addQueryParameter("contenttype", mediaType.toString());
        }

        return new Request.Builder().url(urlBuilder.build()).get().build();
    }

    @Override
    public Call<Void> download(FileLink fileLink, DataSink sink) {
        return download(fileLink, sink, null);
    }

    @Override
    public Call<Void> download(FileLink fileLink, final DataSink sink, final ProgressListener listener) {
        if (fileLink == null) {
            throw new IllegalArgumentException("FileLink argument cannot be null.");
        }

        if (sink == null) {
            throw new IllegalArgumentException("DataSink argument cannot be null.");
        }

        Request request = newDownloadRequest(fileLink);

        return newCall(request, new ResponseAdapter<Void>() {
            @Override
            public Void adapt(Response response) throws IOException, ApiError {
                try {
                    if (response.code() == 404) {
                        throw new FileNotFoundException(
                                "The requested file cannot be found or the file link has expired.");
                    }

                    BufferedSource source = getAsRawBytes(response);
                    if (listener != null) {
                        ProgressListener realListener = listener;
                        if (callbackExecutor != null) {
                            realListener = new ExecutorProgressListener(listener, callbackExecutor);
                        }

                        source = Okio.buffer(new ProgressCountingSource(source, response.body().contentLength(),
                                realListener, progressCallbackThresholdBytes));
                    }

                    sink.readAll(source);
                    return null;
                } finally {
                    closeQuietly(response);
                }
            }
        });
    }

    @Override
    public Call<BufferedSource> download(RemoteFile file) {
        if (file == null) {
            throw new IllegalArgumentException("RemoteFile argument cannot be null.");
        }

        DownloadOptions options = DownloadOptions.create().skipFilename(false).contentType(file.contentType())
                .build();
        return newCall(newDownloadLinkRequest(file.fileId(), options), new ResponseAdapter<BufferedSource>() {
            @Override
            public BufferedSource adapt(Response response) throws IOException, ApiError {
                FileLink link = getAsFileLink(response);
                return newDownloadCall(link);
            }
        });
    }

    @Override
    public Call<BufferedSource> download(FileLink fileLink) {
        if (fileLink == null) {
            throw new IllegalArgumentException("FileLink argument cannot be null.");
        }

        return newCall(newDownloadRequest(fileLink), new ResponseAdapter<BufferedSource>() {
            @Override
            public BufferedSource adapt(Response response) throws IOException, ApiError {
                return getAsRawBytes(response);
            }
        });
    }

    @Override
    public Call<RemoteFile> copyFile(long fileId, long toFolderId) {
        return copyFile(fileId, toFolderId, false);
    }

    @Override
    public Call<RemoteFile> copyFile(long fileId, long toFolderId, boolean overwrite) {
        FormBody.Builder builder = new FormBody.Builder().add("fileid", String.valueOf(fileId)).add("tofolderid",
                String.valueOf(toFolderId));

        if (!overwrite) {
            builder.add("noover", String.valueOf(1));
        }

        Request request = newRequest().url(API_BASE_URL.newBuilder().addPathSegment("copyfile").build())
                .post(builder.build()).build();

        return newCall(request, new ResponseAdapter<RemoteFile>() {
            @Override
            public RemoteFile adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFileResponse.class).getFile();
            }
        });
    }

    @Override
    public Call<RemoteFile> copyFile(RemoteFile file, RemoteFolder toFolder) {
        return copyFile(file, toFolder, false);
    }

    @Override
    public Call<RemoteFile> copyFile(RemoteFile file, RemoteFolder toFolder, boolean overwrite) {
        if (file == null) {
            throw new IllegalArgumentException("file argument cannot be null.");
        }
        if (toFolder == null) {
            throw new IllegalArgumentException("toFolder argument cannot be null.");
        }

        return copyFile(file.fileId(), toFolder.folderId(), overwrite);
    }

    @Override
    public Call<? extends RemoteEntry> copy(RemoteEntry file, RemoteFolder toFolder) {
        return copy(file, toFolder, false);
    }

    @Override
    public Call<? extends RemoteEntry> copy(RemoteEntry file, RemoteFolder toFolder, boolean overwriteFiles) {
        if (file == null) {
            throw new IllegalArgumentException("RemoteEntry argument cannot be null.");
        }
        if (toFolder == null) {
            throw new IllegalArgumentException("RemoteFolder argument cannot be null.");
        }
        final long toFolderId = toFolder.folderId();
        return file.isFolder() ? copyFolder(file.asFolder().folderId(), toFolderId, overwriteFiles)
                : copyFile(file.asFile().fileId(), toFolderId, overwriteFiles);
    }

    @Override
    public Call<? extends RemoteEntry> copy(String id, long toFolderId) {
        return copy(id, toFolderId, false);
    }

    @Override
    public Call<? extends RemoteEntry> copy(String id, long toFolderId, boolean overwriteFiles) {
        if (id == null) {
            throw new IllegalArgumentException("File identifier argument cannot be null.");
        }
        return isFile(id) ? copyFile(toFileId(id), toFolderId, overwriteFiles)
                : copyFile(toFolderId(id), toFolderId, overwriteFiles);
    }

    @Override
    public Call<? extends RemoteEntry> move(RemoteEntry file, RemoteFolder toFolder) {
        if (file == null) {
            throw new IllegalArgumentException("RemoteEntry argument cannot be null.");
        }
        if (toFolder == null) {
            throw new IllegalArgumentException("RemoteFolder argument cannot be null.");
        }

        final long toFolderId = toFolder.folderId();
        return file.isFolder() ? moveFolder(file.asFolder().folderId(), toFolderId)
                : moveFile(file.asFile().fileId(), toFolderId);
    }

    @Override
    public Call<? extends RemoteEntry> move(String id, long toFolderId) {
        if (id == null) {
            throw new IllegalArgumentException("File identifier argument cannot be null.");
        }
        return isFile(id) ? moveFile(toFileId(id), toFolderId) : moveFolder(toFolderId(id), toFolderId);
    }

    @Override
    public Call<Boolean> delete(RemoteEntry file) {
        if (file == null) {
            throw new IllegalArgumentException("RemoteEntry argument cannot be null.");
        }
        return file.isFolder() ? deleteFolder(file.asFolder().folderId()) : deleteFile(file.asFile().fileId());
    }

    @Override
    public Call<Boolean> delete(String id) {
        if (id == null) {
            throw new IllegalArgumentException("File identifier argument cannot be null.");
        }
        return isFile(id) ? deleteFile(toFileId(id)) : deleteFile(toFolderId(id));
    }

    @Override
    public Call<? extends RemoteEntry> rename(RemoteEntry file, String newFilename) {
        if (file == null) {
            throw new IllegalArgumentException("RemoteEntry argument cannot be null.");
        }
        if (newFilename == null) {
            throw new IllegalArgumentException("New filename argument cannot be null.");
        }

        return (file.isFolder() ? renameFolder(file.asFolder().folderId(), newFilename)
                : renameFile(file.asFile().fileId(), newFilename));
    }

    @Override
    public Call<? extends RemoteEntry> rename(String id, String newFilename) {
        if (id == null) {
            throw new IllegalArgumentException("File identifier argument cannot be null.");
        }
        return isFile(id) ? renameFile(toFileId(id), newFilename) : renameFolder(toFolderId(id), newFilename);
    }

    @Override
    public Call<RemoteFile> moveFile(long fileId, long toFolderId) {
        RequestBody body = new FormBody.Builder().add("fileid", String.valueOf(fileId))
                .add("tofolderid", String.valueOf(toFolderId)).build();

        Request request = newRequest().url(API_BASE_URL.newBuilder().addPathSegment("renamefile").build())
                .post(body).build();

        return newCall(request, new ResponseAdapter<RemoteFile>() {
            @Override
            public RemoteFile adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFileResponse.class).getFile();
            }
        });
    }

    @Override
    public Call<RemoteFile> moveFile(RemoteFile file, RemoteFolder toFolder) {
        if (file == null) {
            throw new IllegalArgumentException("file argument cannot be null.");
        }
        if (toFolder == null) {
            throw new IllegalArgumentException("toFolder argument cannot be null.");
        }

        return moveFile(file.fileId(), toFolder.folderId());
    }

    @Override
    public Call<RemoteFile> renameFile(long fileId, String newFilename) {
        if (newFilename == null) {
            throw new IllegalArgumentException("newFileName argument cannot be null.");
        }

        RequestBody body = new FormBody.Builder().add("fileid", String.valueOf(fileId)).add("toname", newFilename)
                .build();

        Request request = newRequest().url(API_BASE_URL.newBuilder().addPathSegment("renamefile").build())
                .post(body).build();

        return newCall(request, new ResponseAdapter<RemoteFile>() {
            @Override
            public RemoteFile adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFileResponse.class).getFile();
            }
        });
    }

    @Override
    public Call<RemoteFile> renameFile(RemoteFile file, String newFilename) {
        if (file == null) {
            throw new IllegalArgumentException("file argument cannot be null.");
        }

        return renameFile(file.fileId(), newFilename);
    }

    @Override
    public Call<RemoteFolder> createFolder(RemoteFolder parentFolder, String folderName) {
        if (parentFolder == null) {
            throw new IllegalArgumentException("folder argument cannot be null.");
        }
        return createFolder(parentFolder.folderId(), folderName);
    }

    @Override
    public Call<RemoteFolder> createFolder(long parentFolderId, String folderName) {
        if (folderName == null) {
            throw new IllegalArgumentException("Folder name is null");
        }
        return newCall(createFolderRequest(parentFolderId, folderName), new ResponseAdapter<RemoteFolder>() {
            @Override
            public RemoteFolder adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFolderResponse.class).getFolder();
            }
        });
    }

    private Request createFolderRequest(long parentFolderId, String folderName) {
        RequestBody body = new FormBody.Builder().add("folderid", String.valueOf(parentFolderId))
                .add("name", folderName).build();

        return newRequest().url(API_BASE_URL.newBuilder().addPathSegment("createfolder").build()).post(body)
                .build();
    }

    @Override
    public Call<Boolean> deleteFolder(RemoteFolder folder) {
        if (folder == null) {
            throw new IllegalArgumentException("folder argument cannot be null.");
        }
        return deleteFolder(folder.folderId(), false);
    }

    @Override
    public Call<Boolean> deleteFolder(RemoteFolder folder, boolean recursively) {
        if (folder == null) {
            throw new IllegalArgumentException("folder argument cannot be null.");
        }
        return deleteFolder(folder.folderId(), recursively);
    }

    @Override
    public Call<Boolean> deleteFolder(long folderId) {
        return deleteFolder(folderId, false);
    }

    @Override
    public Call<Boolean> deleteFolder(long folderId, boolean recursively) {
        RequestBody body = new FormBody.Builder().add("folderid", String.valueOf(folderId)).build();

        Request request = newRequest()
                .url(API_BASE_URL.newBuilder()
                        .addPathSegment(recursively ? "deletefolderrecursive" : "deletefolder").build())
                .post(body).build();

        return newCall(request, new ResponseAdapter<Boolean>() {
            @Override
            public Boolean adapt(Response response) throws IOException, ApiError {
                getAsApiResponse(response, ApiResponse.class);
                return true;
            }
        });
    }

    @Override
    public Call<RemoteFolder> renameFolder(RemoteFolder folder, String newFolderName) {
        if (folder == null) {
            throw new IllegalArgumentException("folder argument cannot be null.");
        }
        return renameFolder(folder.folderId(), newFolderName);
    }

    @Override
    public Call<RemoteFolder> renameFolder(long folderId, String newFolderName) {
        if (newFolderName == null) {
            throw new IllegalArgumentException("Folder name is null");
        }
        return newCall(createRenameFolderRequest(folderId, newFolderName), new ResponseAdapter<RemoteFolder>() {
            @Override
            public RemoteFolder adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFolderResponse.class).getFolder();
            }
        });
    }

    private Request createRenameFolderRequest(long folderId, String newFolderName) {
        RequestBody body = new FormBody.Builder().add("folderid", String.valueOf(folderId))
                .add("toname", newFolderName).build();

        return newRequest().url(API_BASE_URL.newBuilder().addPathSegment("renamefolder").build()).post(body)
                .build();
    }

    @Override
    public Call<RemoteFolder> moveFolder(RemoteFolder folder, RemoteFolder toFolder) {
        if (folder == null || toFolder == null) {
            throw new IllegalArgumentException("folder argument cannot be null.");
        }
        return moveFolder(folder.folderId(), toFolder.folderId());
    }

    @Override
    public Call<RemoteFolder> moveFolder(long folderId, long toFolderId) {
        RequestBody body = new FormBody.Builder().add("folderid", String.valueOf(folderId))
                .add("tofolderid", String.valueOf(toFolderId)).build();

        Request request = newRequest().url(API_BASE_URL.newBuilder().addPathSegment("renamefolder").build())
                .post(body).build();

        return newCall(request, new ResponseAdapter<RemoteFolder>() {
            @Override
            public RemoteFolder adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFolderResponse.class).getFolder();
            }
        });
    }

    @Override
    public Call<RemoteFolder> copyFolder(RemoteFolder folder, RemoteFolder toFolder) {
        if (folder == null || toFolder == null) {
            throw new IllegalArgumentException("folder argument cannot be null.");
        }
        return copyFolder(folder.folderId(), toFolder.folderId(), false);
    }

    @Override
    public Call<RemoteFolder> copyFolder(RemoteFolder folder, RemoteFolder toFolder, boolean overwrite) {
        if (folder == null || toFolder == null) {
            throw new IllegalArgumentException("folder argument cannot be null.");
        }
        return copyFolder(folder.folderId(), toFolder.folderId(), overwrite);
    }

    @Override
    public Call<RemoteFolder> copyFolder(long folderId, long toFolderId) {
        return copyFolder(folderId, toFolderId, false);
    }

    @Override
    public Call<RemoteFolder> copyFolder(long folderId, long toFolderId, boolean overwrite) {
        FormBody.Builder builder = new FormBody.Builder().add("folderid", String.valueOf(folderId))
                .add("tofolderid", String.valueOf(toFolderId));

        if (!overwrite) {
            builder.add("noover", String.valueOf(1));
            builder.add("skipexisting", String.valueOf(1));
        }

        Request request = newRequest().url(API_BASE_URL.newBuilder().addPathSegment("copyfolder").build())
                .post(builder.build()).build();

        return newCall(request, new ResponseAdapter<RemoteFolder>() {
            @Override
            public RemoteFolder adapt(Response response) throws IOException, ApiError {
                return getAsApiResponse(response, GetFolderResponse.class).getFolder();
            }
        });
    }

    @Override
    public Call<UserInfo> getUserInfo() {
        Request request = newRequest().url(API_BASE_URL.newBuilder().addPathSegment("userinfo").build()).get()
                .build();

        return newCall(request, new ResponseAdapter<UserInfo>() {
            @Override
            public UserInfo adapt(Response response) throws IOException, ApiError {
                UserInfoResponse body = getAsApiResponse(response, UserInfoResponse.class);
                return new RealUserInfo(body.getUserId(), body.getEmail(), body.isEmailVerified(),
                        body.getTotalQuota(), body.getUsedQuota());
            }
        });
    }

    @Override
    public RealApiServiceBuilder newBuilder() {
        return new RealApiServiceBuilder(httpClient, callbackExecutor, progressCallbackThresholdBytes,
                authenticator);
    }

    @Override
    public Executor callbackExecutor() {
        return callbackExecutor;
    }

    @Override
    public Dispatcher dispatcher() {
        return httpClient.dispatcher();
    }

    @Override
    public ConnectionPool connectionPool() {
        return httpClient.connectionPool();
    }

    @Override
    public Cache cache() {
        return httpClient.cache();
    }

    @Override
    public int readTimeoutMs() {
        return httpClient.readTimeoutMillis();
    }

    @Override
    public int writeTimeoutMs() {
        return httpClient.writeTimeoutMillis();
    }

    @Override
    public int connectTimeoutMs() {
        return httpClient.connectTimeoutMillis();
    }

    @Override
    public long progressCallbackThreshold() {
        return progressCallbackThresholdBytes;
    }

    @Override
    public Authenticator authenticator() {
        return authenticator;
    }

    @Override
    public void shutdown() {
        this.httpClient.connectionPool().evictAll();
        this.httpClient.dispatcher().executorService().shutdownNow();
        closeQuietly(this.httpClient.cache());
    }

    private Request.Builder newRequest() {
        return new Request.Builder().url(API_BASE_URL);
    }

    private Request createListFolderRequest(long folderId, boolean recursive) {
        HttpUrl.Builder urlBuilder = API_BASE_URL.newBuilder().addPathSegment("listfolder")
                .addQueryParameter("folderid", String.valueOf(folderId))
                .addQueryParameter("noshares", String.valueOf(1));
        if (recursive) {
            urlBuilder.addEncodedQueryParameter("recursive", String.valueOf(1));
        }

        return newRequest().url(urlBuilder.build()).get().build();
    }

    private <T> Call<T> newCall(Request request, ResponseAdapter<T> adapter) {
        Call<T> apiCall = new OkHttpCall<>(httpClient.newCall(request), adapter);
        if (callbackExecutor != null) {
            return new ScheduledCall<>(apiCall, callbackExecutor);
        } else {
            return apiCall;
        }
    }

    private <T extends ApiResponse> T getAsApiResponse(Response response, Class<? extends T> bodyType)
            throws IOException, ApiError {
        T body = deserializeResponseBody(response, bodyType);
        if (body == null) {
            throw new IOException("API returned an empty response body.");
        }
        if (body.isSuccessful()) {
            return body;
        } else {
            throw new ApiError(body.getStatusCode(), body.getMessage());
        }
    }

    private <T> T deserializeResponseBody(Response response, Class<? extends T> bodyType) throws IOException {
        try {
            if (!response.isSuccessful()) {
                throw new APIHttpException(response.code(), response.message());
            }

            JsonReader reader = new JsonReader(
                    new BufferedReader(new InputStreamReader(response.body().byteStream())));
            try {
                return gson.fromJson(reader, bodyType);
            } catch (JsonSyntaxException e) {
                throw new IOException("Malformed JSON response.", e);
            } finally {
                closeQuietly(reader);
            }
        } finally {
            closeQuietly(response);
        }
    }

    private Request newDownloadRequest(FileLink link) {
        return new Request.Builder().url(link.bestUrl()).get().build();
    }

    private BufferedSource newDownloadCall(FileLink fileLink) throws IOException {
        return newDownloadCall(newDownloadRequest(fileLink));
    }

    private BufferedSource newDownloadCall(Request request) throws IOException {
        Response response = httpClient.newCall(request).execute();
        return getAsRawBytes(response);
    }

    private BufferedSource getAsRawBytes(Response response) throws APIHttpException {
        boolean callWasSuccessful = false;
        try {
            if (response.isSuccessful()) {
                callWasSuccessful = true;
                return response.body().source();
            } else {
                throw new APIHttpException(response.code(), response.message());
            }
        } finally {
            if (!callWasSuccessful) {
                closeQuietly(response);
            }
        }
    }
}