/*
* Copyright (c) 2010 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.api.client.http;
import com.google.api.client.util.ArrayValueMap;
import com.google.api.client.util.Charsets;
import com.google.api.client.util.ClassInfo;
import com.google.api.client.util.Data;
import com.google.api.client.util.FieldInfo;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.ObjectParser;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.Throwables;
import com.google.api.client.util.Types;
import com.google.api.client.util.escape.CharEscapers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Implements support for HTTP form content encoding parsing of type
* {@code application/x-www-form-urlencoded} as specified in the <a href=
* "http://www.w3.org/TR/1998/REC-html40-19980424/interact/forms.html#h-17.13.4.1" >HTML 4.0
* Specification</a>.
*
* <p>
* Implementation is thread-safe.
* </p>
*
* <p>
* The data is parsed using {@link #parse(String, Object)}.
* </p>
*
* <p>
* Sample usage:
* </p>
*
* <pre>
static void setParser(HttpTransport transport) {
transport.addParser(new UrlEncodedParser());
}
* </pre>
*
* @since 1.0
* @author Yaniv Inbar
*/
public class UrlEncodedParser implements ObjectParser {
/** {@code "application/x-www-form-urlencoded"} content type. */
public static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
/**
* {@code "application/x-www-form-urlencoded"} media type with UTF-8 encoding.
*
* @since 1.13
*/
public static final String MEDIA_TYPE =
new HttpMediaType(UrlEncodedParser.CONTENT_TYPE).setCharsetParameter(Charsets.UTF_8).build();
/**
* Parses the given URL-encoded content into the given data object of data key name/value pairs
* using {@link #parse(Reader, Object)}.
*
* @param content URL-encoded content or {@code null} to ignore content
* @param data data key name/value pairs
*/
public static void parse(String content, Object data) {
if (content == null) {
return;
}
try {
parse(new StringReader(content), data);
} catch (IOException exception) {
// I/O exception not expected on a string
throw Throwables.propagate(exception);
}
}
/**
* Parses the given URL-encoded content into the given data object of data key name/value pairs,
* including support for repeating data key names.
*
* <p>
* Declared fields of a "primitive" type (as defined by {@link Data#isPrimitive(Type)} are parsed
* using {@link Data#parsePrimitiveValue(Type, String)} where the {@link Class} parameter is the
* declared field class. Declared fields of type {@link Collection} are used to support repeating
* data key names, so each member of the collection is an additional data key value. They are
* parsed the same as "primitive" fields, except that the generic type parameter of the collection
* is used as the {@link Class} parameter.
* </p>
*
* <p>
* If there is no declared field for an input parameter name, it will be ignored unless the input
* {@code data} parameter is a {@link Map}. If it is a map, the parameter value will be stored
* either as a string, or as a {@link ArrayList}<String> in the case of repeated parameters.
* </p>
*
* @param reader URL-encoded reader
* @param data data key name/value pairs
*
* @since 1.14
*/
public static void parse(Reader reader, Object data) throws IOException {
Class<?> clazz = data.getClass();
ClassInfo classInfo = ClassInfo.of(clazz);
List<Type> context = Arrays.<Type>asList(clazz);
GenericData genericData = GenericData.class.isAssignableFrom(clazz) ? (GenericData) data : null;
@SuppressWarnings("unchecked")
Map<Object, Object> map = Map.class.isAssignableFrom(clazz) ? (Map<Object, Object>) data : null;
ArrayValueMap arrayValueMap = new ArrayValueMap(data);
StringWriter nameWriter = new StringWriter();
StringWriter valueWriter = new StringWriter();
boolean readingName = true;
mainLoop: while (true) {
int read = reader.read();
switch (read) {
case -1:
// falls through
case '&':
// parse name/value pair
String name = CharEscapers.decodeUri(nameWriter.toString());
if (name.length() != 0) {
String stringValue = CharEscapers.decodeUri(valueWriter.toString());
// get the field from the type information
FieldInfo fieldInfo = classInfo.getFieldInfo(name);
if (fieldInfo != null) {
Type type =
Data.resolveWildcardTypeOrTypeVariable(context, fieldInfo.getGenericType());
// type is now class, parameterized type, or generic array type
if (Types.isArray(type)) {
// array that can handle repeating values
Class<?> rawArrayComponentType =
Types.getRawArrayComponentType(context, Types.getArrayComponentType(type));
arrayValueMap.put(fieldInfo.getField(), rawArrayComponentType,
parseValue(rawArrayComponentType, context, stringValue));
} else if (Types.isAssignableToOrFrom(
Types.getRawArrayComponentType(context, type), Iterable.class)) {
// iterable that can handle repeating values
@SuppressWarnings("unchecked")
Collection<Object> collection = (Collection<Object>) fieldInfo.getValue(data);
if (collection == null) {
collection = Data.newCollectionInstance(type);
fieldInfo.setValue(data, collection);
}
Type subFieldType = type == Object.class ? null : Types.getIterableParameter(type);
collection.add(parseValue(subFieldType, context, stringValue));
} else {
// parse into a field that assumes it is a single value
fieldInfo.setValue(data, parseValue(type, context, stringValue));
}
} else if (map != null) {
// parse into a map: store as an ArrayList of values
@SuppressWarnings("unchecked")
ArrayList<String> listValue = (ArrayList<String>) map.get(name);
if (listValue == null) {
listValue = new ArrayList<String>();
if (genericData != null) {
genericData.set(name, listValue);
} else {
map.put(name, listValue);
}
}
listValue.add(stringValue);
}
}
// ready to read next name/value pair
readingName = true;
nameWriter = new StringWriter();
valueWriter = new StringWriter();
if (read == -1) {
break mainLoop;
}
break;
case '=':
// finished with name, now read value
readingName = false;
break;
default:
// read one more character
if (readingName) {
nameWriter.write(read);
} else {
valueWriter.write(read);
}
}
}
arrayValueMap.setValues();
}
private static Object parseValue(Type valueType, List<Type> context, String value) {
Type resolved = Data.resolveWildcardTypeOrTypeVariable(context, valueType);
return Data.parsePrimitiveValue(resolved, value);
}
public <T> T parseAndClose(InputStream in, Charset charset, Class<T> dataClass)
throws IOException {
InputStreamReader r = new InputStreamReader(in, charset);
return parseAndClose(r, dataClass);
}
public Object parseAndClose(InputStream in, Charset charset, Type dataType) throws IOException {
InputStreamReader r = new InputStreamReader(in, charset);
return parseAndClose(r, dataType);
}
@SuppressWarnings("unchecked")
public <T> T parseAndClose(Reader reader, Class<T> dataClass) throws IOException {
return (T) parseAndClose(reader, (Type) dataClass);
}
public Object parseAndClose(Reader reader, Type dataType) throws IOException {
Preconditions.checkArgument(
dataType instanceof Class<?>, "dataType has to be of type Class<?>");
Object newInstance = Types.newInstance((Class<?>) dataType);
parse(new BufferedReader(reader), newInstance);
return newInstance;
}
}