com.scvngr.levelup.core.net.AbstractRequest.java Source code

Java tutorial

Introduction

Here is the source code for com.scvngr.levelup.core.net.AbstractRequest.java

Source

/*
 * Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp
 *
 * 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.scvngr.levelup.core.net;

import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.scvngr.levelup.core.annotation.LevelUpApi;
import com.scvngr.levelup.core.annotation.LevelUpApi.Contract;
import com.scvngr.levelup.core.util.CoreLibConstants;
import com.scvngr.levelup.core.util.LogManager;
import com.scvngr.levelup.core.util.NullUtils;
import com.scvngr.levelup.core.util.PreconditionUtil;

import net.jcip.annotations.Immutable;

import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;

import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

/**
 * Object which represents an HTTP request.
 */
@Immutable
@LevelUpApi(contract = Contract.PUBLIC)
public abstract class AbstractRequest implements Parcelable {

    /**
     * The method of the HTTP request.
     */
    @NonNull
    private final HttpMethod mMethod;

    /**
     * The URL string where the request will be send. This URL does not yet encode
     * {@link #mQueryParams}.
     */
    @NonNull
    private final String mUrlString;

    /**
     * Key-value pairs representing HTTP headers. The elements stored in this map are not yet URL
     * encoded.
     * <p>
     * This map cannot contain null keys or null values.
     * <p>
     * This field has been wrapped in a call to {@link Collections#unmodifiableMap(Map)}.
     */
    @NonNull
    private final Map<String, String> mRequestHeaders;

    /**
     * Key-value pairs representing query string parameters. The elements stored in this map are not
     * yet URL encoded.
     * <p>
     * This map cannot contain null keys or null values.
     * <p>
     * This field has been wrapped in a call to {@link Collections#unmodifiableMap(Map)}.
     */
    @NonNull
    private final Map<String, String> mQueryParams;

    /**
     * Create a new {@link AbstractRequest}.
     * <p>
     * Note that there are some representation invariants for the {@link AbstractRequest} object.
     * For example, a {@link HttpMethod#GET} cannot contain a {@code body}.
     *
     * @param method the {@code HttpMethod} of the request type.
     * @param url the URL to request (and to append query string parameters to).
     * @param requestHeaders the headers to add to the request. This cannot contain null keys or
     *        null values.
     * @param queryParams the query string parameters. This cannot contain null keys or null values.
     */
    public AbstractRequest(@NonNull final HttpMethod method, @NonNull final String url,
            @Nullable final Map<String, String> requestHeaders, @Nullable final Map<String, String> queryParams) {

        PreconditionUtil.assertNotNull(method, "method");
        PreconditionUtil.assertNotNull(url, "url");

        if (null != requestHeaders) {
            mRequestHeaders = NullUtils
                    .nonNullContract(Collections.unmodifiableMap(new HashMap<String, String>(requestHeaders)));
        } else {
            mRequestHeaders = NullUtils.nonNullContract(Collections.unmodifiableMap(new HashMap<String, String>()));
        }

        if (null != queryParams) {
            mQueryParams = NullUtils
                    .nonNullContract(Collections.unmodifiableMap(new HashMap<String, String>(queryParams)));
        } else {
            mQueryParams = NullUtils.nonNullContract(Collections.unmodifiableMap(new HashMap<String, String>()));
        }

        mUrlString = url;
        mMethod = method;

        checkRep();
    }

    /**
     * Create a new {@link AbstractRequest} from an absolute URL.
     *
     * @param method the {@code HttpMethod} of the request type.
     * @param url The URL to request. This can include query parameters.
     * @param requestHeaders the headers to add to the request. This cannot contain null keys or
     *        null values.
     * @throws IllegalArgumentException if the URI passed in isn't an absolute URL.
     */
    public AbstractRequest(@NonNull final HttpMethod method, @NonNull final Uri url,
            @Nullable final Map<String, String> requestHeaders) throws IllegalArgumentException {
        this(method, stripQueryParameters(url), requestHeaders, extractQueryParameters(url));
    }

    /**
     * @param url the URL whose query parameters will be extracted.
     * @return a map of the query parameters or null if there is an error parsing the URL.
     */
    @Nullable
    private static Map<String, String> extractQueryParameters(final Uri url) {
        Map<String, String> params = null;

        try {
            final List<NameValuePair> paramsList = URLEncodedUtils.parse(new URI(url.toString()), "utf-8");
            params = new HashMap<String, String>(paramsList.size());

            for (final NameValuePair nvp : paramsList) {
                params.put(nvp.getName(), nvp.getValue());
            }
        } catch (final URISyntaxException e) {
            // failsafe
            LogManager.e(NullUtils.format("could not parse uri: '%s'. " + "dropping query parameters.", url), e);
        }

        return params;
    }

    /**
     * Returns the portion of an absolute URL before the query string. E.g.
     * {@code http://example.com/search?q=kittens} would return {@code "http://example.com/search"}.
     *
     * @param url the URL to strip.
     * @return the URL stripped of any query parameters, including the '?'.
     * @throws IllegalArgumentException if the URI passed in isn't an absolute URL.
     */
    @NonNull
    private static String stripQueryParameters(final Uri url) throws IllegalArgumentException {
        if (!url.isAbsolute() || !url.isHierarchical()) {
            throw new IllegalArgumentException("Request URI must be an absolute URL");
        }

        return NullUtils.nonNullContract(url.buildUpon().query(null).build().toString());
    }

    /**
     * Constructor for parceling.
     *
     * @param in the parcel to read from
     */
    public AbstractRequest(@NonNull final Parcel in) {
        mMethod = NullUtils.nonNullContract(HttpMethod.valueOf(in.readString()));
        mUrlString = NullUtils.nonNullContract(in.readString());

        final Map<String, String> headers = new HashMap<String, String>();
        in.readMap(headers, HashMap.class.getClassLoader());
        mRequestHeaders = NullUtils.nonNullContract(Collections.unmodifiableMap(headers));

        final Map<String, String> query = new HashMap<String, String>();
        in.readMap(query, HashMap.class.getClassLoader());
        mQueryParams = NullUtils.nonNullContract(Collections.unmodifiableMap(query));

        checkRep();
    }

    /**
     * Asserts representation invariants of this class.
     */
    @SuppressWarnings({ "unused", "null" })
    private void checkRep() {
        if (CoreLibConstants.IS_CHECKREP_ENABLED) {
            if (null == mMethod) {
                throw new NullPointerException("mMethod cannot be null");
            }

            if (null == mUrlString) {
                throw new NullPointerException("mUrlString cannot be null");
            }

            if (null == mRequestHeaders) {
                throw new NullPointerException("mUrlString cannot be null");
            }

            for (final Entry<String, String> entry : mRequestHeaders.entrySet()) {
                if (null == entry.getKey()) {
                    throw new NullPointerException("mRequestHeaders cannot contain null keys");
                }

                if (null == entry.getValue()) {
                    throw new NullPointerException("mRequestHeaders cannot contain null values");
                }
            }

            if (null == mQueryParams) {
                throw new NullPointerException("mQueryParams");
            }

            for (final Entry<String, String> entry : mQueryParams.entrySet()) {
                if (null == entry.getKey()) {
                    throw new NullPointerException("mQueryParams cannot contain null keys");
                }

                if (null == entry.getValue()) {
                    throw new NullPointerException("mQueryParams cannot contain null values");
                }
            }
        }
    }

    /**
     * @param context the context to use to get context dependent headers.
     * @return the HTTP headers for this request. This has been wrapped in a call to
     *         {@link Collections#unmodifiableMap(Map)}.
     */
    @NonNull
    public Map<String, String> getRequestHeaders(@NonNull final Context context) {
        return mRequestHeaders;
    }

    /**
     * @param context the context to use to get context dependent parameters.
     * @return the parameters encoded in the query string. This has been wrapped in a call to
     *         {@link Collections#unmodifiableMap(Map)}.
     */
    @NonNull
    public Map<String, String> getQueryParams(@NonNull final Context context) {
        return mQueryParams;
    }

    /**
     * @param context Application context.
     * @return the base URL String.
     * @throws BadRequestException if the request is invalid.
     */
    @NonNull
    public String getUrlString(@NonNull final Context context) throws BadRequestException {
        return mUrlString;
    }

    /**
     * @param context the context to use to get context dependent parameters
     * @return the final {@link URL} to request, including query string parameters.
     * @throws BadRequestException if the request is invalid.
     */
    @NonNull
    public final URL getUrl(@NonNull final Context context) throws BadRequestException {
        URL url = null;
        final Map<String, String> queryParams = getQueryParams(context);
        final Uri.Builder builder = Uri.parse(getUrlString(context)).buildUpon();
        /*
         * Sort the query params by their keys, this is not part of the public interface and is
         * subject to change. We do this for testing purposes.
         */
        final Set<String> keys = new TreeSet<String>(queryParams.keySet());

        for (final String key : keys) {
            builder.appendQueryParameter(key, queryParams.get(key));
        }

        try {
            url = new URL(builder.build().toString());
        } catch (final MalformedURLException e) {
            LogManager.e("MalformedUrlException when getting request url", e);
            final BadRequestException e2 = new BadRequestException(
                    "MalformedUrlException when getting request url");
            e2.initCause(e);
            throw e2;
        }

        return url;
    }

    /**
     * @return the {@link HttpMethod} for the request
     */
    public final HttpMethod getMethod() {
        return mMethod;
    }

    /**
     * Subclasses must implement this write the POST body (if it has one) to the
     * {@link OutputStream} passed.
     *
     * @param context the Application context.
     * @param stream the {@link OutputStream} to write the POST body to.
     * @throws IOException if writing to the {@link OutputStream} fails
     */
    public abstract void writeBodyToStream(@NonNull final Context context, @NonNull final OutputStream stream)
            throws IOException;

    /**
     * @param context the Application context.
     * @return the length of the request body if there is one.
     */
    public abstract int getBodyLength(@NonNull final Context context);

    //@formatter:off
    @SuppressWarnings("null")
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((null == mMethod) ? 0 : mMethod.hashCode());
        result = prime * result + ((null == mQueryParams) ? 0 : mQueryParams.hashCode());
        result = prime * result + ((null == mRequestHeaders) ? 0 : mRequestHeaders.hashCode());
        result = prime * result + ((null == mUrlString) ? 0 : mUrlString.hashCode());
        return result;
    }

    @SuppressWarnings({ "unused", "null" })
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }

        if (null == obj) {
            return false;
        }

        if (!(obj instanceof AbstractRequest)) {
            return false;
        }

        final AbstractRequest other = (AbstractRequest) obj;
        if (mMethod != other.mMethod) {
            return false;
        }

        if (null == mQueryParams) {
            if (null != other.mQueryParams) {
                return false;
            }
        } else if (!mQueryParams.equals(other.mQueryParams)) {
            return false;
        }

        if (null == mRequestHeaders) {
            if (null != other.mRequestHeaders) {
                return false;
            }
        } else if (!mRequestHeaders.equals(other.mRequestHeaders)) {
            return false;
        }

        if (null == mUrlString) {
            if (null != other.mUrlString) {
                return false;
            }
        } else if (!mUrlString.equals(other.mUrlString)) {
            return false;
        }

        return true;
    }
    //@formatter:on

    @Override
    public String toString() {
        return String.format(Locale.US,
                "AbstractRequest [mMethod=%s, mUrlString=%s, mRequestHeaders=%s, mQueryParams=%s]", mMethod,
                mUrlString, mRequestHeaders, mQueryParams);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(final Parcel dest, final int flags) {
        dest.writeString(mMethod.name());
        dest.writeString(mUrlString);
        dest.writeMap(mRequestHeaders);
        dest.writeMap(mQueryParams);
    }

    /**
     * Exception that is thrown if a request is invalid at the time it it sent.
     */
    public static final class BadRequestException extends Exception {

        /**
         * Implements {@link java.io.Serializable}.
         */
        private static final long serialVersionUID = 8423248708984803306L;

        /**
         * Constructor.
         *
         * @param detailMessage the message to display in the backtrace.
         */
        public BadRequestException(final String detailMessage) {
            super(detailMessage);
        }
    }
}