/* Copyright (c) 2008 Google Inc. * * 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.google.gdata.util.common.net; import static com.google.gdata.util.common.base.Preconditions.checkNotNull; import com.google.common.collect.ForwardingMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimaps; import com.google.gdata.util.httputil.FormUrlDecoder; import java.io.IOException; import java.io.Serializable; import java.nio.charset.Charset; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Represents a sequence of name-value pairs encoded using the * application/x-www-form-urlencoded content type, typically as the query part * of a URI, as defined by section 17.13.4 of the W3C's <a * href="http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1">HTML * 4.01 Specification</a>. * * <p>This class stores keys and values in unicode (decoded) form, allowing * clients to get and set parameters safely using normal strings. Encoding is * performed when {@link #toString(Charset)} is called. See the {@link * UriEncoder} class comments for an important discussion regarding encoding. * * <p>Parameter maps may not contain null values. Both keys and values are * allowed to be the empty string, though in most cases only values are the * empty string. Parsing a query string may be a "lossy" operation in the * trivial sense, in that the original string cannot be exactly reconstructed; * for example, the query string "foo=&bar" is considered equivalent to * "foo&bar". Also note that parsing the empty string will <i>not</i> return an * empty map but will return a map with a single entry of the empty string as * both key and value. The empty map (see {@link #EMPTY_MAP}) represents an * undefined query. * * <p>In addition to the {@link ListMultimap} API, this class provides a * convenient {@link #getFirst} method for retrieving the first value of a * parameter, for when you expect only a single parameter of the specified name. * * <p>Parameter maps are typically constructed either by {@code Uri} or {@code * UriBuilder}. However, you may also construct a parameter map from scratch * using the constructor, or by calling {@link #parse(String)}. * * @see UriBuilder#getQueryParameters() * @see Uri#getQueryParameters() * */ public final class UriParameterMap extends ForwardingMultimap<String, String> implements ListMultimap<String, String>, Cloneable, Serializable { private static final long serialVersionUID = -3053773769157973706L; /** The immutable empty map. */ public static final UriParameterMap EMPTY_MAP; static { EMPTY_MAP = new UriParameterMap( ImmutableMultimap.<String, String>of()); } private final ListMultimap<String, String> delegate; /** * Constructs a new parameter map backed by the specified map. Private because * this constructor should only be used when the specified map is immutable or * "owned" by this instance. * * @throws NullPointerException if {@code delegate} is null */ private UriParameterMap(ListMultimap<String, String> delegate) { this.delegate = delegate; } /** Constructs a new empty parameter map. */ public UriParameterMap() { this(LinkedListMultimap.<String,String>create()); } /** * Constructs a new parameter map populated with parameters parsed from the * specified query string using the {@link UriEncoder#DEFAULT_ENCODING}, * UTF-8. * * @param query the query string, e.g., "q=flowers&n=20" * @return a mutable parameter map representing the query string * @throws NullPointerException if {@code query} is null */ public static UriParameterMap parse(String query) { return parse(query, UriEncoder.DEFAULT_ENCODING); } /** * Constructs a new parameter map populated with parameters parsed from the * specified query string using the specified encoding. * * @param query the query string, e.g., "q=flowers&n=20" * @param encoding the character encoding to use * @return a mutable parameter map representing the query string * @throws NullPointerException if any argument is null */ public static UriParameterMap parse(String query, Charset encoding) { checkNotNull(query); UriParameterMap map = new UriParameterMap(); map.merge(query, encoding); return map; } /** * Returns an unmodifiable view of the specified parameter map. This method * allows modules to provide users with "read-only" access to internal * parameter maps. Query operations on the returned map "read through" to the * specified map, and attempts to modify the returned map, whether direct or * via its iterator or collection views, result in an {@code * UnsupportedOperationException}. * * @param map the parameter map for which to return an unmodifiable view * @return an unmodifiable view of the specified parameter map * @throws NullPointerException if {@code map} is null */ public static UriParameterMap unmodifiableMap(UriParameterMap map) { return new UriParameterMap( Multimaps.unmodifiableListMultimap(map.delegate())); } protected ListMultimap<String, String> delegate() { return delegate; } /** * Populates the parameter map from the specified query using the specified * encoding. Package-private so that it can be used by {@link * UriBuilder#setQuery(String)}; it might be reasonable to make this method * public, though. */ // this package and make it private @SuppressWarnings("deprecation") void merge(String query, Charset encoding) { checkNotNull(query); checkNotNull(encoding); FormUrlDecoder.parseWithCallback(query, encoding.name(), new FormUrlDecoder.Callback() { public void handleParameter(String name, String value) { put(name, value); } }); } /** * Returns the first parameter value for the specified {@code key} (parameter * name) or {@code null} if no parameters are defined for that key. If the * parameter is defined, equivalent to {@code get(key).get(0)}. * * @param key the name of the parameter * @return the value of the parameter if present, or null * @see javax.servlet.ServletRequest#getParameter(String) */ public String getFirst(String key) { checkNotNull(key); List<String> values = get(key); return values.isEmpty() ? null : values.get(0); } /** * Appends the string representation of these parameters to the specified * string builder using the specified encoding. * * @param out the string builder to append to * @param encoding the character encoding to use * @throws NullPointerException if any argument is null */ public void appendTo(StringBuilder out, Charset encoding) { try { appendTo((Appendable) out, encoding); } catch (IOException e) { throw new AssertionError(e); // StringBuilder doesn't throw IOException } } /** * Appends the string representation of these parameters to the specified * {@code Appendable} using the specified encoding. * * @param out the appendable to append to * @param encoding the character encoding to use * @throws NullPointerException if any argument is null * @throws IOException if the {@link Appendable} encounters an error */ public void appendTo(Appendable out, Charset encoding) throws IOException { checkNotNull(out); for (Iterator<Map.Entry<String, String>> i = entries().iterator(); i.hasNext();) { Map.Entry<String, String> entry = i.next(); out.append(UriEncoder.encode(entry.getKey(), encoding)); if (!"".equals(entry.getValue())) { out.append("="); out.append(UriEncoder.encode(entry.getValue(), encoding)); } if (i.hasNext()) { out.append("&"); } } } @SuppressWarnings("unchecked") @Override public UriParameterMap clone() { /* * The supertype is not cloneable. But copy-construct is safe because this * is a final class. */ ListMultimap<String,String> multimap = LinkedListMultimap.<String,String>create(delegate()); return new UriParameterMap(multimap); } /** * Returns the string representation of these parameters using the specified * encoding, e.g., "q=flowers&n=20". * * @param encoding the character encoding to use * @throws NullPointerException if {@code encoding} is null */ public String toString(Charset encoding) { StringBuilder out = new StringBuilder(); appendTo(out, encoding); return out.toString(); } /** * Returns an immutable copy of this parameter map as a {@code Map} from * strings to string arrays. */ public Map<String, String[]> copyToArrayMap() { ImmutableMap.Builder<String, String[]> builder = ImmutableMap.builder(); Map<String, Collection<String>> delegateMap = delegate().asMap(); for (Map.Entry<String, Collection<String>> entry : delegateMap.entrySet()) { Collection<String> values = entry.getValue(); builder.put(entry.getKey(), values.toArray(new String[values.size()])); } return builder.build(); } /** * Returns the string representation of these parameters using the {@link * UriEncoder#DEFAULT_ENCODING}, UTF-8, e.g., "q=flowers&n=20". */ @Override public String toString() { return toString(UriEncoder.DEFAULT_ENCODING); } /* This class extends ForwardingMultimap but implements ListMultimap! */ @Override public List<String> get(String key) { return delegate().get(key); } @Override public List<String> removeAll(Object key) { return delegate().removeAll(key); } @Override public List<String> replaceValues(String key, Iterable<? extends String> values) { return delegate().replaceValues(key, values); } }