com.tinspx.util.net.FormEncodedBody.java Source code

Java tutorial

Introduction

Here is the source code for com.tinspx.util.net.FormEncodedBody.java

Source

/* Copyright (C) 2013-2014 Ian Teune <ian.teune@gmail.com>
 * 
 * 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.tinspx.util.net;

import com.google.common.base.Charsets;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Predicates;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.escape.Escaper;
import com.google.common.net.HttpHeaders;
import com.google.common.net.UrlEscapers;
import com.tinspx.util.collect.Listenable;
import com.tinspx.util.collect.Predicated;
import com.tinspx.util.io.ByteArrayChannelSource;
import java.nio.charset.Charset;
import java.util.Collections;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import lombok.NonNull;
import lombok.experimental.Delegate;

/**
 * A {@code RequestBody} that encodes parameters as
 * <a href="http://goo.gl/OQEc8">{@code application/x-www-form-urlencoded}</a>
 * form data.
 * <p>
 * Parameters may be added through {@link #add(String, String)} or
 * directly on the modifiable {@code ListMultimap} returned from
 * {@link #parameters()}. Parameter order is maintained (parameters
 * are included in the output in the same order they are added). Any headers may
 * be added to the modifiable {@code Multimap} returned from {@link #headers()}.
 * <p>
 * If non-standard escaping or encoding is needed, then
 * {@link #escaper(Escaper)}, {@link #charset(Charset)}, and
 * {@link #contentType(String)} can be used to control every part of the
 * parameter encoding process.
 *
 * @see UrlEscapers#urlFormParameterEscaper()
 * @author Ian
 */
@NotThreadSafe
public class FormEncodedBody extends RequestBody {

    /**
     * {@code application/x-www-form-urlencoded}
     */
    public static final String X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";

    private Escaper escaper;
    private Charset charset;
    private ListMultimap<String, String> headers;
    private ListMultimap<String, String> headersView;
    private final ListMultimap<String, String> params;
    /**
     * a view of {@link #params} returned to the user in order to detect
     * changes and disallow null keys.
     */
    private ListMultimap<String, String> paramsView;
    private StringBuilder sb;
    /**
     * the final encoded content, set to null when the parameters are modified
     * or when the escaper or charset is changed
     */
    private byte[] array;

    @Delegate
    final ByteArrayChannelSource delegate;
    {
        //lombok causes compilation error unless assigned here
        delegate = new ByteArrayChannelSource() {
            @Override
            public byte[] read() {
                if (array == null) {
                    array = encode();
                }
                return array;
            }
        };
    }

    public FormEncodedBody() {
        params = LinkedListMultimap.create();
    }

    private FormEncodedBody(FormEncodedBody f) {
        escaper = f.escaper;
        charset = f.charset;
        if (f.headers != null) {
            headers = LinkedListMultimap.create(f.headers);
        }
        params = LinkedListMultimap.create(f.params);
        array = f.array;
    }

    @Override
    public FormEncodedBody duplicate() {
        return new FormEncodedBody(this);
    }

    /**
     * Sets the {@link Escaper} used to escape the parameter names and values.
     * Defaults to {@link UrlEscapers#urlFormParameterEscaper()}. If this
     * {@code Escaper} produces non {@code US-ASCII} characters and the content
     * should not be encoded as {@code UTF-8}, set an appropriate
     * {@code Charset} through {@link #charset(Charset)}. The escaper typically
     * should not be changed.
     */
    public FormEncodedBody escaper(@NonNull Escaper escaper) {
        if (!Objects.equal(this.escaper, escaper)) {
            this.escaper = escaper;
            array = null;
        }
        return this;
    }

    /**
     * Returns the {@code Escaper} being used to escape parameter names and
     * values. Unless explicitly set through {@link #escaper(Escaper)} (which
     * typically should not be necessary), the returned {@code Escaper} will be
     * {@link UrlEscapers#urlFormParameterEscaper()}.
     */
    public Escaper escaper() {
        return MoreObjects.firstNonNull(escaper, UrlEscapers.urlFormParameterEscaper());
    }

    /**
     * Sets the {@code Charset} used to encode the escaped parameter names and
     * values. Defaults {@code UTF-8}. Unless a non-standard {@code Escaper} is
     * set through {@link #escaper(Escaper)}, the {@code Charset} does not need
     * to be changed.
     */
    public FormEncodedBody charset(@NonNull Charset charset) {
        if (!Objects.equal(this.charset, charset)) {
            this.charset = charset;
            array = null;
        }
        return this;
    }

    /**
     * Returns the {@code Charset} being used to encode the escaped parameter
     * names and values. Unless explicitly set through {@link #charset(Charset)}
     * (which typically should not be necessary), the returned {@code Charset}
     * will be {@code UTF-8}.
     */
    public Charset charset() {
        return MoreObjects.firstNonNull(charset, Charsets.UTF_8);
    }

    /**
     * Sets the {@code Content-Type} header returned from
     * {@link #headers() headers()}. Defaults to
     * {@code application/x-www-form-urlencoded}. The {@code Content-Type}
     * should typically not need to be explicitly set.
     */
    public FormEncodedBody contentType(@NonNull String contentType) {
        headers().replaceValues(HttpHeaders.CONTENT_TYPE, Collections.singleton(contentType));
        return this;
    }

    /**
     * adds the specified {@code name=value} parameter.
     * 
     * @throws NullPointerException if name is null
     */
    public FormEncodedBody add(@NonNull String name, @Nullable String value) {
        params.put(name, value);
        array = null;
        return this;
    }

    /**
     * Returns a modifiable view of the parameters.
     */
    public ListMultimap<String, String> parameters() {
        if (paramsView == null) {
            paramsView = Predicated.listMultimap(Listenable.listMultimap(params, new Listenable.Modification() {
                @Override
                public void onModify(Object src, Listenable.Event type) {
                    array = null;
                }
                //don't track entries as values can be null
            }), Predicates.notNull(), Predicates.alwaysTrue(), false);
        }
        return paramsView;
    }

    /**
     * Returns a modifiable view of the headers. Any header (except
     * {@code Content-Length}) may be added, including overwriting the
     * {@code Content-Type} (which defaults to
     * {@code application/x-www-form-urlencoded}).
     */
    @Override
    public ListMultimap<String, String> headers() {
        if (headers == null) {
            headers = LinkedListMultimap.create();
            headers.put(HttpHeaders.CONTENT_TYPE, X_WWW_FORM_URLENCODED);
        }
        if (headersView == null) {
            headersView = Predicated.listMultimap(headers, Requests.HKEY_NO_LEN, Predicates.notNull());
        }
        return headersView;
    }

    public FormEncodedBody header(@NonNull String name, @NonNull String value) {
        headers().replaceValues(name, Collections.singleton(value));
        return this;
    }

    public FormEncodedBody addHeader(@NonNull String name, @NonNull String value) {
        headers().put(name, value);
        return this;
    }

    private byte[] encode() {
        if (sb == null) {
            sb = new StringBuilder(Math.max(params.size() * 16, 32));
        } else {
            sb.setLength(0);
        }
        Request.append(sb, params, escaper());
        return sb.toString().getBytes(charset());
    }

    @Override
    public String toString() {
        return String.format("FormEncodedBody{%s%s%sparameters=%s}",
                escaper != null ? "escaper=" + escaper + ", " : "",
                charset != null ? "charset=" + charset + ", " : "",
                headers != null ? "headers=" + headers + ", " : "", params);
    }
}