// ------------------------------------------------------------------------------
// Copyright (c) 2014 Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// ------------------------------------------------------------------------------

package com.microsoft.live;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.AsyncTask;
import android.text.TextUtils;

/**
 * {@code LiveConnectClient} is a class that is responsible for making requests over to the
 * Live Connect REST API. In order to perform requests, a {@link LiveConnectSession} is required.
 * A {@link LiveConnectSession} can be created from a {@link LiveAuthClient}.
 *
 * {@code LiveConnectClient} provides methods to perform both synchronous and asynchronous calls
 * on the Live Connect REST API. A synchronous method's corresponding asynchronous method is
 * suffixed with "Async" (e.g., the synchronous method, get, has a corresponding asynchronous
 * method called, getAsync). Asynchronous methods require a call back listener that will be called
 * back on the main/UI thread on completion, error, or progress.
 */
public class LiveConnectClient {

    /** Gets the ContentLength when a request finishes and sets it in the given operation. */
    private static class ContentLengthObserver implements ApiRequest.Observer {
        private final LiveDownloadOperation operation;

        public ContentLengthObserver(LiveDownloadOperation operation) {
            assert operation != null;

            this.operation = operation;
        }

        @Override
        public void onComplete(HttpResponse response) {
            Header header = response.getFirstHeader(HTTP.CONTENT_LEN);

            // Sometimes this header is not included in the response.
            if (header == null) {
                return;
            }

            int contentLength = Integer.valueOf(header.getValue());

            this.operation.setContentLength(contentLength);
        }
    }

    /**
     * Listens to an {@link ApiRequestAsync} for onComplete and onError events and calls the proper
     * method on the given {@link LiveDownloadOperationListener} on a given event.
     */
    private static class DownloadObserver implements ApiRequestAsync.Observer<InputStream> {
        private final LiveDownloadOperationListener listener;
        private final LiveDownloadOperation operation;

        public DownloadObserver(LiveDownloadOperation operation,
                                LiveDownloadOperationListener listener) {
            assert operation != null;
            assert listener != null;

            this.operation = operation;
            this.listener = listener;
        }

        @Override
        public void onComplete(InputStream result) {
            this.operation.setStream(result);
            this.listener.onDownloadCompleted(this.operation);
        }

        @Override
        public void onError(LiveOperationException e) {
            this.listener.onDownloadFailed(e, this.operation);
        }
    }

    /**
     * Listens to an {@link ApiRequestAsync} for onComplete and onError events and calls the proper
     * method on the given {@link LiveDownloadOperationListener} on a given event. When the download
     * is complete this writes the results to a file, and publishes progress updates.
     */
    private static class FileDownloadObserver extends AsyncTask<InputStream, Integer, Runnable>
                                              implements ApiRequestAsync.Observer<InputStream> {
        private class OnErrorRunnable implements Runnable {
            private final LiveOperationException exception;

            public OnErrorRunnable(LiveOperationException exception) {
                this.exception = exception;
            }

            @Override
            public void run() {
                listener.onDownloadFailed(exception, operation);
            }
        }

        private final File file;
        private final LiveDownloadOperationListener listener;
        private final LiveDownloadOperation operation;

        public FileDownloadObserver(LiveDownloadOperation operation,
                                    LiveDownloadOperationListener listener,
                                    File file) {
            assert operation != null;
            assert listener != null;
            assert file != null;

            this.operation = operation;
            this.listener = listener;
            this.file = file;
        }

        @Override
        protected Runnable doInBackground(InputStream... params) {
            InputStream is = params[0];

            byte[] buffer = new byte[BUFFER_SIZE];

            OutputStream out;
            try {
                out = new BufferedOutputStream(new FileOutputStream(file));
            } catch (FileNotFoundException e) {
                LiveOperationException exception =
                        new LiveOperationException(ErrorMessages.CLIENT_ERROR, e);
                return new OnErrorRunnable(exception);
            }

            try {
                int totalBytes = operation.getContentLength();
                int bytesRemaining = totalBytes;

                int bytesRead;
                while ((bytesRead = is.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);

                    bytesRemaining -= bytesRead;
                    publishProgress(totalBytes, bytesRemaining);
                }
            } catch (IOException e) {
                LiveOperationException exception =
                        new LiveOperationException(ErrorMessages.CLIENT_ERROR, e);
                return new OnErrorRunnable(exception);
            } finally {
                closeSilently(out);
                closeSilently(is);
            }

            return new Runnable() {
                @Override
                public void run() {
                    listener.onDownloadCompleted(operation);
                }
            };
        }

        @Override
        protected void onPostExecute(Runnable result) {
            result.run();
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            int totalBytes = values[0];
            int bytesRemaining = values[1];

            assert totalBytes >= 0;
            assert bytesRemaining >= 0;
            assert totalBytes >= bytesRemaining;

            listener.onDownloadProgress(totalBytes, bytesRemaining, operation);
        }

        @Override
        public void onComplete(InputStream result) {
            this.execute(result);
        }

        @Override
        public void onError(LiveOperationException e) {
            this.listener.onDownloadFailed(e, this.operation);
        }
    }

    /**
     * Listens to an {@link ApiRequestAsync} for onComplete and onError events and calls the proper
     * method on the given {@link LiveOperationListener} on a given event.
     */
    private static class OperationObserver implements ApiRequestAsync.Observer<JSONObject> {

        private final LiveOperationListener listener;
        private final LiveOperation operation;

        public OperationObserver(LiveOperation operation,
                                 LiveOperationListener listener) {
            assert operation != null;
            assert listener != null;

            this.operation = operation;
            this.listener = listener;
        }

        @Override
        public void onComplete(JSONObject result) {
            this.operation.setResult(result);
            this.listener.onComplete(this.operation);
        }

        @Override
        public void onError(LiveOperationException e) {
            this.listener.onError(e, this.operation);
        }
    }

    /** non-instantiable class that contains static constants for parameter names. */
    private static final class ParamNames {
        public static final String ACCESS_TOKEN = "session.getAccessToken()";
        public static final String BODY = "body";
        public static final String DESTINATION = "destination";
        public static final String FILE = "file";
        public static final String FILENAME = "filename";
        public static final String OVERWRITE = "overwrite";
        public static final String PATH = "path";
        public static final String SESSION = "session";

        private ParamNames() { throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS); }
    }

    private enum SessionState {
        LOGGED_IN {
            @Override
            public void check() {
                // nothing. valid state.
            }
        },
        LOGGED_OUT {
            @Override
            public void check() {
                throw new IllegalStateException(ErrorMessages.LOGGED_OUT);
            }
        };

        public abstract void check();
    }

    /**
     * Listens to an {@link ApiRequestAsync} for onComplete and onError events, and listens to an
     * {@link EntityEnclosingApiRequest} for onProgress events and calls the
     * proper {@link LiveUploadOperationListener} on such events.
     */
    private static class UploadRequestListener implements ApiRequestAsync.Observer<JSONObject>,
                                                          ApiRequestAsync.ProgressObserver {

        private final LiveUploadOperationListener listener;
        private final LiveOperation operation;

        public UploadRequestListener(LiveOperation operation,
                                     LiveUploadOperationListener listener) {
            assert operation != null;
            assert listener != null;

            this.operation = operation;
            this.listener = listener;
        }

        @Override
        public void onComplete(JSONObject result) {
            this.operation.setResult(result);
            this.listener.onUploadCompleted(this.operation);
        }

        @Override
        public void onError(LiveOperationException e) {
            assert e != null;

            this.listener.onUploadFailed(e, this.operation);
        }

        @Override
        public void onProgress(Long... values) {
            long totalBytes = values[0].longValue();
            long numBytesWritten = values[1].longValue();

            assert totalBytes >= 0L;
            assert numBytesWritten >= 0L;
            assert numBytesWritten <= totalBytes;

            long bytesRemaining = totalBytes - numBytesWritten;
            this.listener.onUploadProgress((int)totalBytes, (int)bytesRemaining, this.operation);
        }
    }

    private static int BUFFER_SIZE = 1 << 10;
    private static int CONNECT_TIMEOUT_IN_MS = 30 * 1000;

    /** The key used for HTTP MOVE and HTTP COPY requests. */
    private static final String DESTINATION_KEY = "destination";

    private static volatile HttpClient HTTP_CLIENT;
    private static Object HTTP_CLIENT_LOCK = new Object();

    /**
     * A LiveDownloadOperationListener that does nothing on each of the call backs.
     * This is used so when a null listener is passed in, this can be used, instead of null,
     * to avoid if (listener == null) checks.
     */
    private static final LiveDownloadOperationListener NULL_DOWNLOAD_OPERATION_LISTENER;

    /**
     * A LiveOperationListener that does nothing on each of the call backs.
     * This is used so when a null listener is passed in, this can be used, instead of null,
     * to avoid if (listener == null) checks.
     */
    private static final LiveOperationListener NULL_OPERATION_LISTENER;

    /**
     * A LiveUploadOperationListener that does nothing on each of the call backs.
     * This is used so when a null listener is passed in, this can be used, instead of null,
     * to avoid if (listener == null) checks.
     */
    private static final LiveUploadOperationListener NULL_UPLOAD_OPERATION_LISTENER;

    private static int SOCKET_TIMEOUT_IN_MS = 30 * 1000;

    static {
        NULL_DOWNLOAD_OPERATION_LISTENER = new LiveDownloadOperationListener() {
            @Override
            public void onDownloadCompleted(LiveDownloadOperation operation) {
                assert operation != null;
            }

            @Override
            public void onDownloadFailed(LiveOperationException exception,
                                         LiveDownloadOperation operation) {
                assert exception != null;
                assert operation != null;
            }

            @Override
            public void onDownloadProgress(int totalBytes,
                                           int bytesRemaining,
                                           LiveDownloadOperation operation) {
                assert totalBytes >= 0;
                assert bytesRemaining >= 0;
                assert totalBytes >= bytesRemaining;
                assert operation != null;
            }
        };

        NULL_OPERATION_LISTENER = new LiveOperationListener() {
            @Override
            public void onComplete(LiveOperation operation) {
                assert operation != null;
            }

            @Override
            public void onError(LiveOperationException exception, LiveOperation operation) {
                assert exception != null;
                assert operation != null;
            }
        };

        NULL_UPLOAD_OPERATION_LISTENER = new LiveUploadOperationListener() {
            @Override
            public void onUploadCompleted(LiveOperation operation) {
                assert operation != null;
            }

            @Override
            public void onUploadFailed(LiveOperationException exception,
                                       LiveOperation operation) {
                assert exception != null;
                assert operation != null;
            }

            @Override
            public void onUploadProgress(int totalBytes,
                                         int bytesRemaining,
                                         LiveOperation operation) {
                assert totalBytes >= 0;
                assert bytesRemaining >= 0;
                assert totalBytes >= bytesRemaining;
                assert operation != null;
            }
        };
    }

    /**
     * Checks to see if the given path is a valid uri.
     *
     * @param path to check.
     * @return the valid URI object.
     */
    private static URI assertIsUri(String path) {
        try {
            return new URI(path);
        } catch (URISyntaxException e) {
            String message = String.format(ErrorMessages.INVALID_URI, ParamNames.PATH);
            throw new IllegalArgumentException(message);
        }
    }

    /**
     * Checks to see if the path is null, empty, or a valid uri.
     *
     * This method will be used for Download and Upload requests.
     * This method will NOT be used for Copy, Delete, Get, Move, Post and Put requests.
     *
     * @param path object_id to check.
     * @throws IllegalArgumentException if the path is empty or an invalid uri.
     * @throws NullPointerException if the path is null.
     */
    private static void assertValidPath(String path) {
        LiveConnectUtils.assertNotNullOrEmpty(path, ParamNames.PATH);
        assertIsUri(path);
    }

    private static void closeSilently(Closeable c) {
        try {
            c.close();
        } catch (Exception e) {
            // Silently...ssshh
        }
    }

    /**
     * Checks to see if the path is null, empty, or is an absolute uri and throws
     * the proper exception if it is.
     *
     * This method will be used for Copy, Delete, Get, Move, Post, and Put requests.
     * This method will NOT be used for Download and Upload requests.
     *
     * @param path object_id to check.
     * @throws IllegalArgumentException if the path is empty or an absolute uri.
     * @throws NullPointerException if the path is null.
     */
    private static void assertValidRelativePath(String path) {
        LiveConnectUtils.assertNotNullOrEmpty(path, ParamNames.PATH);

        if (path.toLowerCase().startsWith("http") || path.toLowerCase().startsWith("https")) {
            String message = String.format(ErrorMessages.ABSOLUTE_PARAMETER, ParamNames.PATH);
            throw new IllegalArgumentException(message);
        }
    }

    /**
     * Creates a new JSONObject body that has one key-value pair.
     * @param key
     * @param value
     * @return a new JSONObject body with one key-value pair.
     */
    private static JSONObject createJsonBody(String key, String value) {
        Map<String, String> tempBody = new HashMap<String, String>();
        tempBody.put(key, value);
        return new JSONObject(tempBody);
    }

    private static HttpClient getHttpClient() {
        // The LiveConnectClients can share one HttpClient with a ThreadSafeConnManager.
        if (HTTP_CLIENT == null) {
            synchronized (HTTP_CLIENT_LOCK) {
                if (HTTP_CLIENT == null) {
                    HttpParams params = new BasicHttpParams();
                    HttpConnectionParams.setConnectionTimeout(params, CONNECT_TIMEOUT_IN_MS);
                    HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT_IN_MS);

                    ConnManagerParams.setMaxTotalConnections(params, 100);
                    HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

                    SchemeRegistry schemeRegistry = new SchemeRegistry();
                    schemeRegistry.register(new Scheme("http",
                                                       PlainSocketFactory.getSocketFactory(),
                                                       80));
                    schemeRegistry.register(new Scheme("https",
                                                       SSLSocketFactory.getSocketFactory(),
                                                       443));

                    // Create an HttpClient with the ThreadSafeClientConnManager.
                    // This connection manager must be used if more than one thread will
                    // be using the HttpClient, which is a common scenario.
                    ClientConnectionManager cm =
                            new ThreadSafeClientConnManager(params, schemeRegistry);
                    HTTP_CLIENT = new DefaultHttpClient(cm, params);
                }
            }
        }

        return HTTP_CLIENT;
    }

    /**
     * Constructs a new LiveOperation and calls the listener's onError method.
     *
     * @param e
     * @param listener
     * @param userState arbitrary object that is used to determine the caller of the method.
     * @return a new LiveOperation
     */
    private static LiveOperation handleException(String method,
                                                 String path,
                                                 LiveOperationException e,
                                                 LiveOperationListener listener,
                                                 Object userState) {
        LiveOperation operation =
                new LiveOperation.Builder(method, path).userState(userState).build();
        OperationObserver requestListener =
                new OperationObserver(operation, listener);

        requestListener.onError(e);
        return operation;
    }

    /**
     * Constructs a new LiveOperation and calls the listener's onUploadFailed method.
     *
     * @param e
     * @param listener
     * @param userState arbitrary object that is used to determine the caller of the method.
     * @return a new LiveOperation
     */
    private static LiveOperation handleException(String method,
                                                    String path,
                                                    LiveOperationException e,
                                                    LiveUploadOperationListener listener,
                                                    Object userState) {
        LiveOperation operation =
                new LiveOperation.Builder(method, path).userState(userState).build();
        UploadRequestListener requestListener = new UploadRequestListener(operation, listener);

        requestListener.onError(e);
        return operation;
    }

    /**
     * Converts an InputStream to a {@code byte[]}.
     *
     * @param is to convert to a {@code byte[]}.
     * @return a new {@code byte[]} from the InputStream.
     * @throws IOException if there was an error reading or closing the InputStream.
     */
    private static byte[] toByteArray(InputStream is) throws IOException {
        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        OutputStream out = new BufferedOutputStream(byteOut);
        is = new BufferedInputStream(is);
        byte[] buffer = new byte[BUFFER_SIZE];

        try {
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        } finally {
            // we want to perform silent close operations
            closeSilently(is);
            closeSilently(out);
        }

        return byteOut.toByteArray();
    }

    /** Change this to mock the HTTP responses. */
    private HttpClient httpClient;

    private final LiveConnectSession session;
    private SessionState sessionState;

    /**
     * Constructs a new {@code LiveConnectClient} instance and initializes it.
     *
     * @param session that will be used to authenticate calls over to the Live Connect REST API.
     * @throws NullPointerException if session is null or if session.getAccessToken() is null.
     * @throws IllegalArgumentException if session.getAccessToken() is empty.
     */
    public LiveConnectClient(LiveConnectSession session) {
        LiveConnectUtils.assertNotNull(session, ParamNames.SESSION);

        String accessToken = session.getAccessToken();
        LiveConnectUtils.assertNotNullOrEmpty(accessToken, ParamNames.ACCESS_TOKEN);

        this.session = session;
        this.sessionState = SessionState.LOGGED_IN;

        // set a listener for the accessToken. If it is set to null, then the session was logged
        // out.
        this.session.addPropertyChangeListener("accessToken", new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent event) {
                String newValue = (String)event.getNewValue();

                if (TextUtils.isEmpty(newValue)) {
                    LiveConnectClient.this.sessionState = SessionState.LOGGED_OUT;
                } else {
                    LiveConnectClient.this.sessionState = SessionState.LOGGED_IN;
                }
            }
        });

        this.httpClient = getHttpClient();
    }

    /**
     * Performs a synchronous HTTP COPY on the Live Connect REST API.
     *
     * A COPY duplicates a resource.
     *
     * @param path object_id of the resource to copy.
     * @param destination the folder_id where the resource will be copied to.
     * @return The LiveOperation that contains the JSON result.
     * @throws LiveOperationException if there is an error during the execution of the request.
     * @throws IllegalArgumentException if the path or destination is empty or if the path is an
     *                                  absolute uri.
     * @throws NullPointerException if either the path or destination parameters are null.
     */
    public LiveOperation copy(String path, String destination) throws LiveOperationException {
        assertValidRelativePath(path);
        LiveConnectUtils.assertNotNullOrEmpty(destination, ParamNames.DESTINATION);

        CopyRequest request = this.createCopyRequest(path, destination);
        return execute(request);
    }

    /**
     * Performs an asynchronous HTTP COPY on the Live Connect REST API.
     *
     * A COPY duplicates a resource.
     *
     * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success.
     * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will
     * be called. Both of these methods will be called on the main/UI thread.
     *
     * @param path object_id of the resource to copy.
     * @param destination the folder_id where the resource will be copied to.
     * @param listener called on either completion or error during the copy request.
     * @return the LiveOperation associated with the request.
     * @throws IllegalArgumentException if the path or destination is empty or if the path is an
     *                                  absolute uri.
     * @throws NullPointerException if either the path or destination parameters are null.
     */
    public LiveOperation copyAsync(String path,
                                   String destination,
                                   LiveOperationListener listener) {
        return this.copyAsync(path, destination, listener, null);
    }

    /**
     * Performs an asynchronous HTTP COPY on the Live Connect REST API.
     *
     * A COPY duplicates a resource.
     *
     * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success.
     * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will
     * be called. Both of these methods will be called on the main/UI thread.
     *
     * @param path object_id of the resource to copy.
     * @param destination the folder_id where the resource will be copied to
     * @param listener called on either completion or error during the copy request.
     * @param userState arbitrary object that is used to determine the caller of the method.
     * @return the LiveOperation associated with the request.
     * @throws IllegalArgumentException if the path or destination is empty or if the path is an
     *                                  absolute uri.
     * @throws NullPointerException if either the path or destination parameters are null.
     */
    public LiveOperation copyAsync(String path,
                                   String destination,
                                   LiveOperationListener listener,
                                   Object userState) {
        assertValidRelativePath(path);
        LiveConnectUtils.assertNotNullOrEmpty(destination, ParamNames.DESTINATION);
        if (listener == null) {
            listener = NULL_OPERATION_LISTENER;
        }

        CopyRequest request;
        try {
            request = this.createCopyRequest(path, destination);
        } catch (LiveOperationException e) {
            return handleException(CopyRequest.METHOD, path, e, listener, userState);
        }

        return executeAsync(request, listener, userState);
    }

    /**
     * Performs a synchronous HTTP DELETE on the Live Connect REST API.
     *
     * HTTP DELETE deletes a resource.
     *
     * @param path object_id of the resource to delete.
     * @return The LiveOperation that contains the delete response
     * @throws LiveOperationException if there is an error during the execution of the request.
     * @throws IllegalArgumentException if the path is empty or an absolute uri.
     * @throws NullPointerException if the path is null.
     */
    public LiveOperation delete(String path) throws LiveOperationException {
        assertValidRelativePath(path);

        DeleteRequest request = new DeleteRequest(this.session, this.httpClient, path);

        return execute(request);
    }

    /**
     * Performs an asynchronous HTTP DELETE on the Live Connect REST API.
     *
     * HTTP DELETE deletes a resource.
     *
     * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success.
     * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will
     * be called. Both of these methods will be called on the main/UI thread.
     *
     * @param path object_id of the resource to delete.
     * @param listener called on either completion or error during the delete request.
     * @return the LiveOperation associated with the request.
     * @throws IllegalArgumentException if the path is empty or an absolute uri.
     * @throws NullPointerException if the path is null.
     */
    public LiveOperation deleteAsync(String path, LiveOperationListener listener) {
        return this.deleteAsync(path, listener, null);
    }

    /**
     * Performs an asynchronous HTTP DELETE on the Live Connect REST API.
     *
     * HTTP DELETE deletes a resource.
     *
     * {@link LiveOperationListener#onComplete(LiveOperation)} will be called on success.
     * Otherwise, {@link LiveOperationListener#onError(LiveOperationException, LiveOperation)} will
     * be called. Both of these methods will be called on the main/UI thread.
     *
     * @param path object_id of the resource to delete.
     * @param listener called on either completion or error during the delete request.
     * @param userState arbitrary object that is used to determine the caller of the method.
     * @return the LiveOperation associated with the request.
     * @throws IllegalArgumentException if the path is empty or an absolute uri.
     * @throws NullPointerException if the path is null.
     */
    public LiveOperation deleteAsync(String path,
                                     LiveOperationListener listener,
                                     Object userState) {
        assertValidRelativePath(path);
        if (listener == null) {
            listener = NULL_OPERATION_LISTENER;
        }

        DeleteRequest request = new DeleteRequest(this.session, this.httpClient, path);


        return executeAsync(request, listener, userState);
    }

    /**
     * Downloads a resource by performing a synchronous HTTP GET on the Live Connect REST API that
     * returns the response as an {@link InputStream}.
     *
     * @param path object_id of the resource to download.
     * @throws LiveOperationException if there is an error during the execution of the request.
     * @throws IllegalArgumentException if the path is empty or an invalid uri.
     * @throws NullPointerException if the path is null.
     */
