com.licryle.httpposter.HttpPoster.java Source code

Java tutorial

Introduction

Here is the source code for com.licryle.httpposter.HttpPoster.java

Source

/*
 * Copyright (C) 2015 - Cyrille Berliat <cyrille.berliat+github@gmail.com>
 *
 * Licensed under the GNU General Public  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
 *
 *      https://www.gnu.org/licenses/gpl-2.0.txt
 *
 * 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.licryle.httpposter;

import android.os.AsyncTask;
import android.util.Log;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.Iterator;
import java.util.Map;

/**
 * Main class of the library. Orchestrate the Async HTTP Posting. Only the first
 * argument/HttpConfiguration passed in execute() will be acted on.
 *
 * How to use:
 * - Implement the HttpListener into a class,
 * - Instanciate a {@link HttpConfiguration},
 * - Instanciate a {@link HttpPoster},
 * - Call execute on the {@link HttpConfiguration} object.
 *
 * ** Usage example:
 * HashMap<String, String> mArgs = new HashMap<>();
 * mArgs.put("lat", "40.712784");
 * mArgs.put("lon", "-74.005941");
 *
 * ArrayList<File> aFileList = getMyImageFiles();
 *
 * HttpConfiguration mConf = new HttpConfiguration(
 *     "http://www.mysite.com/HttpPostEndPoint",
 *     mArgs,
 *     aFileList,
 *     this, // If this class implements HttpListener
 *     null,  // Boundary for Entities - Optional
 *     15000  // Timeout in ms for the connection operation
 *     10000, // Timeout in ms for the reading operation
 * );
 *
 * new HttpPoster().execute(mConf);
 *
 * @see com.licryle.httpposter.HttpConfiguration
 * @see com.licryle.httpposter.HttpListener
 *
 * This code is an merge of {@see http://stackoverflow.com/a/19188010} and
 * {@see http://www.xyzws.com/javafaq/how-to-use-httpurlconnection-post-data-to-
 * web-server/139} put in a nice AsyncTask, with configuration and a listener
 * for all events.
 *
 * It is also updated to the newest Android API, as HttpPost and HttpClient
 * classes are deprecated in favor of HttpURLConnection.
 */
public class HttpPoster extends AsyncTask<HttpConfiguration, Integer, Long> {
    /*****************************************************************************
     ****************************** Public Constants *****************************
     ****************************************************************************/

    /** A failure ocurred when trying to open the connection. */
    public static final long FAILURE_CONNECTION = -1;
    /** A failure ocurred when trying to post the request. */
    public static final long FAILURE_TRANSFER = -2;
    /** A failure ocurred when retrieving the reponse from the post request. */
    public static final long FAILURE_RESPONSE = -3;
    /** A file passed in paramater couldn't be accessed. */
    private static final long FAILURE_FILE_READ = -4;

    /** The POST request was successful and we retrieved the answer correctly. */
    public static final long SUCCESS = 0;

    /*****************************************************************************
     **************************** Protected Variables ****************************
     ****************************************************************************/

    /** The response from the server following the HTTPPost request. */
    protected String _sResponse = "";
    /** The response code from the server following the HTTPPost request. 200 is a
     *  success. */
    protected int _iResponseCode = 0;
    /** The listener object to which we will dispatch events. */
    protected HttpListener _mListener = null;

    /** Static Unique ID so we can associate each request with a unique id for the
     * listener's callbacks. */
    protected static int UNIQUE_ID = 0;

    /** Current Unique Id of the POST Request. */
    protected int _iInstanceId = 0;

    /*****************************************************************************
     ************************** Protected Helper Classes *************************
     ****************************************************************************/

    /** HttpPost implementation of HttpEntity to allow for a
     * _ProgressiveOutputStream to be called in place of the generic OutputStream.
     * This way, we can track the progress of the posting.
     *
     * @see com.licryle.httpposter.HttpPoster._ProgressiveOutputStream
     */
    protected class _ProgressiveEntity implements HttpEntity {
        /** Original Entity we want to send in the POST request */
        protected HttpEntity _mEntity;
        /** Poster object we want to inform progress of. */
        protected HttpPoster _mPoster;

        public _ProgressiveEntity(HttpEntity mEntity, HttpPoster mPoster) {
            _mEntity = mEntity;
            _mPoster = mPoster;
        }

        @Override
        public void consumeContent() throws IOException {
            _mEntity.consumeContent();
        }

        @Override
        public InputStream getContent() throws IOException, IllegalStateException {
            return _mEntity.getContent();
        }

        @Override
        public Header getContentEncoding() {
            return _mEntity.getContentEncoding();
        }

        @Override
        public long getContentLength() {
            return _mEntity.getContentLength();
        }

        @Override
        public Header getContentType() {
            return _mEntity.getContentType();
        }

        @Override
        public boolean isChunked() {
            return _mEntity.isChunked();
        }

        @Override
        public boolean isRepeatable() {
            return _mEntity.isRepeatable();
        }

        @Override
        public boolean isStreaming() {
            return _mEntity.isStreaming();
        } // CONSIDER put a _real_ delegator into here!

        @Override
        public void writeTo(OutputStream mOutstream) throws IOException {
            Log.d("HttpPoster", "_ProgressiveEntity.In");
            _mEntity.writeTo(new _ProgressiveOutputStream(mOutstream, _mEntity.getContentLength(), _mPoster));
            Log.d("HttpPoster", "_ProgressiveEntity.Out");
        }

    }

    /** _ProgressiveOutputStream is an implementation of DataOutputStream that
     * keeps track of the number of bytes written in the stream.
     *
     * Only called by _ProgressiveEntity.
     *
     * @see com.licryle.httpposter.HttpPoster._ProgressiveEntity
     */
    protected class _ProgressiveOutputStream extends DataOutputStream {
        /** Number of bytes we already sent in the stream. */
        protected long _lTotalSent;
        /** Total number of bytes we want to send. */
        protected long _lTotalSize;
        /** Poster object we want to inform progress of. */
        protected HttpPoster _mPoster;
        /** Progress from 0 to 100 of the transfer. Used to pace progress sending.*/
        protected int _iProgress;

        public _ProgressiveOutputStream(OutputStream proxy, long total, HttpPoster mPoster) {
            super(proxy);
            _lTotalSent = 0;
            _lTotalSize = total;
            _mPoster = mPoster;
            _iProgress = 0;
        }

        /**
         * Override of the write method from the base class so we can track how many
         * bytes we sent in the stream. Once written, dispatches progress using
         * _dispatchOnProgress as an int from 0 to 100 corresponding to the
         * percentage of data sent, only when the value changes to avoid spamming
         * progress signals.
         *
         * @param aBytes Full array of bytes to be sent in the stream.
         * @param iStart Starting index in aBytes weere we want to start sending.
         * @param iCount Number of bytes to write in the stream.
         * @throws IOException Whenever the write fails, raises an IO Exception.
         */
        @Override
        public void write(byte[] aBytes, int iStart, int iCount) throws IOException {
            _lTotalSent += iCount;

            out.write(aBytes, iStart, iCount);

            int iOldProgress = _iProgress;
            _iProgress = (int) ((_lTotalSent / (float) _lTotalSize) * 100);

            if (_iProgress != iOldProgress) {
                _mPoster.publishProgress(_iProgress);
            }
        }
    }

    /*****************************************************************************
     **************************** AsyncTask Overrides ****************************
     ****************************************************************************/

    @Override
    protected void onPreExecute() {
        _iInstanceId = ++UNIQUE_ID;

        Log.d("HttpPoster", String.format("onPreExecute: Now executing instance %d", _iInstanceId));
    }

    /**
     * Executes on the background thread to perform the HTTP Post request. This
     * function initialize the Listener, builds the Entity to send to the server,
     * then process the request. Warning: only the first {link HttpConfiguration}
     * is processed.
     *
     * @param mConf {@link HttpConfiguration} of the post request to be processed.
     *                                       Only the first element is acted upon.
     * @return The result of the HTTP Post request. Either #SUCCESS,
     * #FAILURE_CONNECTION, #FAILURE_TRANSFER, #FAILURE_RESPONSE,
     * #FAILURE_MALFORMEDURL or #FAILURE_FILE_READ.
     */
    @Override
    protected Long doInBackground(HttpConfiguration... mConf) {
        _mListener = mConf[0].getListener();

        try {
            _ProgressiveEntity mEntity = _buildEntity(mConf[0]);

            return _httpPost(mConf[0], mEntity);
        } catch (IOException e) {
            e.printStackTrace();
            return FAILURE_FILE_READ;
        }
    }

    @Override
    protected void onProgressUpdate(Integer... iProgress) {
        _dispatchOnProgress(iProgress[0]);
    }

    @Override
    protected void onPostExecute(Long lResult) {
        Log.d("HttpPoster", String.format("onPostExecute: Finished executing instance %d", _iInstanceId));

        if (lResult == SUCCESS || lResult == FAILURE_RESPONSE) {
            _dispatchOnSuccess(_sResponse);
        } else {
            _dispatchOnFailure(lResult);
        }
    }

    /*****************************************************************************
     ************************** HttpListener Dispatchers *************************
     ****************************************************************************/

    /**
     * Will call the {@link HttpListener#onStartTransfer} callback should the
     * _mListener object be non-null.
     *
     * @see HttpListener#onStartTransfer
     */
    protected void _dispatchOnStartTransfer() {
        if (_mListener != null) {
            _mListener.onStartTransfer(_iInstanceId);
        }
    }

    /**
     * Will call the {@link HttpListener#onProgress} callback should the
     * _mListener object be non-null.
     *
     * @param iProgress The process of the upload on a scale of 0 to 100.
     *
     * @see HttpListener#onProgress
     */
    protected void _dispatchOnProgress(int iProgress) {
        if (_mListener != null) {
            _mListener.onProgress(_iInstanceId, iProgress);
        }
    }

    /**
     * Will call the {@link HttpListener#onResponse} callback should the
     * _mListener object be non-null.
     *
     * @param sResponse The response from the server.
     *
     * @see HttpListener#onResponse
     */
    protected void _dispatchOnSuccess(String sResponse) {
        if (_mListener != null) {
            _mListener.onResponse(_iInstanceId, _iResponseCode, sResponse);
        }

        _iResponseCode = 0;
    }

    /**
     * Will call the {@link HttpListener#onFailure} callback should the
     * _mListener object be non-null.
     *
     * @param lError The error code related to the POST failure. Either
     *               #FAILURE_CONNECTION, #FAILURE_TRANSFER, #FAILURE_RESPONSE,
     *               #FAILURE_MALFORMEDURL or #FAILURE_FILE_READ.
     *
     * @see HttpListener#onFailure
     */
    protected void _dispatchOnFailure(long lError) {
        if (_mListener != null) {
            _mListener.onFailure(_iInstanceId, lError);
        }
    }

    /*****************************************************************************
     **************************** Core Poster Functions **************************
     ****************************************************************************/

    /**
     * Builds the {@link _ProgressiveEntity} that we will send to the server.
     * Takes for input an {@link HttpConfiguration} that contains the Post
     * variables and File Names to send.
     *
     * @param mConf Configuration of the POST request to be processed.
     * @return A ProgressiveEntity which progress can be tracked as we send it to
     * the server.
     *
     * @throws IOException When a file in the list of files from
     * {@link HttpConfiguration#getFiles()} cannot be read.
     *
     * @see {@link com.licryle.httpposter.HttpPoster._ProgressiveEntity}
     * @see {@link com.licryle.httpposter.HttpConfiguration}
     */
    protected _ProgressiveEntity _buildEntity(HttpConfiguration mConf) throws IOException {
        Log.d("HttpPoster", String.format("_buildEntity: Entering for Instance %d", _iInstanceId));

        /********* Build request content *********/
        MultipartEntityBuilder mBuilder = MultipartEntityBuilder.create();
        mBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        mBuilder.setBoundary(mConf.getHTTPBoundary());

        int iFileNb = 0;
        Iterator mFiles = mConf.getFiles().iterator();
        while (mFiles.hasNext()) {
            final File mFile = (File) mFiles.next();

            try {
                mBuilder.addBinaryBody("file_" + iFileNb, mFile, ContentType.DEFAULT_BINARY, mFile.getName());
            } catch (Exception e) {
                throw new IOException();
            }

            iFileNb++;
        }

        Iterator mArgs = mConf.getArgs().entrySet().iterator();
        while (mArgs.hasNext()) {
            Map.Entry mPair = (Map.Entry) mArgs.next();

            mBuilder.addTextBody((String) mPair.getKey(), (String) mPair.getValue(),
                    ContentType.MULTIPART_FORM_DATA);
        }

        Log.d("HttpPoster", String.format("_buildEntity: Leaving for Instance %d", _iInstanceId));

        return new _ProgressiveEntity(mBuilder.build(), this);
    }

    /**
     * Opens a connection to the POST end point specified in the mConf
     * {@link HttpConfiguration} and sends the content of mEntity. Attempts to
     * read the answer from the server after the POST Request.
     * 
     * @param mConf The {@link HttpConfiguration} of the request.
     * @param mEntity The Entity to send in the HTTP Post. Should be built using
     *                {@link #_buildEntity}.
     *
     * @return The result of the HTTP Post request. Either #SUCCESS,
     * #FAILURE_CONNECTION, #FAILURE_TRANSFER, #FAILURE_RESPONSE,
     * #FAILURE_MALFORMEDURL.
     *
     * @see HttpConfiguration
     * @see _ProgressiveEntity
     */
    protected Long _httpPost(HttpConfiguration mConf, _ProgressiveEntity mEntity) {
        Log.d("HttpPoster", String.format("_httpPost: Entering Instance %d", _iInstanceId));

        /******** Open request ********/
        try {
            HttpURLConnection mConn = (HttpURLConnection) mConf.getEndPoint().openConnection();

            mConn.setRequestMethod("POST");
            mConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + mConf.getHTTPBoundary());

            mConn.setDoInput(true);
            mConn.setDoOutput(true);
            mConn.setUseCaches(false);
            mConn.setReadTimeout(mConf.getReadTimeout());
            mConn.setConnectTimeout(mConf.getConnectTimeout());
            mConn.setInstanceFollowRedirects(false);

            mConn.connect();

            Log.d("HttpPoster", String.format("_httpPost: Connected for Instance %d", _iInstanceId));

            try {
                /********** Write request ********/
                _dispatchOnStartTransfer();

                Log.d("HttpPoster", String.format("_httpPost: Sending for Instance %d", _iInstanceId));

                mEntity.writeTo(mConn.getOutputStream());
                mConn.getOutputStream().flush();
                mConn.getOutputStream().close();

                _iResponseCode = mConn.getResponseCode();
                try {
                    Log.d("HttpPoster", String.format("_httpPost: Reading for Instance %d", _iInstanceId));

                    _readServerAnswer(mConn.getInputStream());
                    return SUCCESS;
                } catch (IOException e) {
                    return FAILURE_RESPONSE;
                }
            } catch (Exception e) {
                Log.d("HTTPParser", e.getMessage());
                e.printStackTrace();

                return FAILURE_TRANSFER;
            } finally {
                if (mConn != null) {
                    Log.d("HttpPoster", String.format("_httpPost: Disconnecting Instance %d", _iInstanceId));

                    mConn.disconnect();
                }
            }
        } catch (Exception e) {
            return FAILURE_CONNECTION;
        }
    }

    /**
     * Reads the content of the InputStream, typically the answer from the server
     * in this context, and stores it in {@link #_sResponse}.
     *
     * @param mInputStream Any valid InputStream.
     * @throws IOException When the InputStream can't be read.
     */
    protected void _readServerAnswer(InputStream mInputStream) throws IOException {
        BufferedReader mRd = new BufferedReader(new InputStreamReader(mInputStream));

        String sLine = "";
        String sContent = "";

        while ((sLine = mRd.readLine()) != null) {
            sContent += sLine + "\n";
        }

        _sResponse = sContent;
    }
}