Java tutorial
/* 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); } }