/* 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.wireformats;
import com.google.common.collect.MapMaker;
import com.google.gdata.client.CoreErrorDomain;
import com.google.gdata.data.DateTime;
import com.google.gdata.util.ParseException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
/**
* Converts values from strings to typed values (objects).
*
* @param <T> data type handled by this converter
*
*/
public abstract class ObjectConverter<T> {
/**
* Map from data type to value convert. This is used for types that don't have
* a string-arg constructor or need special handling.
*/
private static final ConcurrentMap<Class<?>, ObjectConverter<?>> CONVERTERS =
new MapMaker().makeMap();
static {
addConverter(DateTime.class, new DateTimeConverter());
addConverter(Enum.class, new EnumConverter());
addConverter(Boolean.class, new BooleanConverter());
}
/**
* Add converter for a data type.
*
* @param type data type
* @param converter converter
*/
public static <V> void addConverter(
Class<V> type, ObjectConverter<V> converter) {
CONVERTERS.put(type, converter);
}
/**
* Converts an object to the given datatype by casting if possible, and if not
* by narrowing from {@link String} using the registered object converters.
*
* @param <V> the type of value to return.
* @param value the value to convert.
* @param datatype the datatype to convert to.
* @return the original value if it could be cast to the required type, or
* a narrowed version of the value if narrowing was possible.
* @throws ParseException if narrowing was not possible.
*/
public static <V> V getValue(Object value, Class<V> datatype)
throws ParseException {
if (value instanceof String) {
return getValue((String) value, datatype);
}
if (value == null || datatype.isInstance(value)) {
return datatype.cast(value);
}
throw new ParseException("Cannot convert value " + value
+ " of type " + value.getClass() + " to " + datatype);
}
/**
* Translate an untyped (string) value to a typed value.
*
* @param <V> data type
* @param value value
* @param datatype class of value type
* @return typed value
* @throws ParseException if value cannot be parsed according to type
*/
public static <V> V getValue(String value, Class<V> datatype)
throws ParseException {
if (value == null || datatype.isInstance(value)) {
return datatype.cast(value);
}
try {
ObjectConverter<V> handler = getHandler(datatype);
if (handler != null) {
return handler.convertValue(value, datatype);
}
Constructor<V> cons = datatype.getConstructor(String.class);
return cons.newInstance(value);
} catch (NoSuchMethodException e) {
ParseException pe = new ParseException(
CoreErrorDomain.ERR.missingConverter);
pe.setInternalReason("No converter for type " + datatype);
throw pe;
} catch (IllegalArgumentException e) {
throw new ParseException(e);
} catch (InstantiationException e) {
throw new ParseException(e);
} catch (IllegalAccessException e) {
throw new ParseException(e);
} catch (InvocationTargetException e) {
throw new ParseException(e.getTargetException());
}
}
/**
* Translate an untyped (string) value to a typed value.
*
* @param value value to convert.
* @return value converted to type {@code T}.
* @throws ParseException if value cannot be parsed according to type
*/
public abstract T convertValue(String value, Class<? extends T> datatype)
throws ParseException;
/**
* Get handler associated with a data type. Unchecked because we know that
* the class type and the handler type match, but we can't tell the compiler.
*
* @param type data type to retrieve a handler for.
* @return an object converter that can convert to type {@code V}.
*/
@SuppressWarnings("unchecked")
private static <V> ObjectConverter<V> getHandler(Class<? extends V> type) {
ObjectConverter<V> handler = (ObjectConverter<V>) CONVERTERS.get(type);
if (handler == null && type.isEnum()) {
handler = (ObjectConverter<V>) CONVERTERS.get(Enum.class);
}
return handler;
}
/**
* Object converter for {@link DateTime}.
*/
private static class DateTimeConverter extends ObjectConverter<DateTime> {
@Override
public DateTime convertValue(String value,
Class<? extends DateTime> datatype) throws ParseException {
try {
return DateTime.parseDateTimeChoice(value);
} catch (NumberFormatException e) {
throw new ParseException(
CoreErrorDomain.ERR.invalidDatetime, e);
}
}
}
/**
* Object converter for {@link Enum} instances. Unchecked because
* we don't actually care about the return type from
* {@link Enum#valueOf(Class, String)}, but it requires stronger typing than
* we can give it.
*/
@SuppressWarnings("unchecked")
private static class EnumConverter extends ObjectConverter<Enum> {
@Override
public Enum<?> convertValue(String value, Class<? extends Enum> datatype)
throws ParseException {
if (value == null) {
return null;
}
Enum<?> result = Enum.valueOf(datatype, value.toUpperCase());
if (result == null) {
throw new ParseException(
CoreErrorDomain.ERR.invalidEnumValue.withInternalReason(
"No such enum of type " + datatype + " named "
+ value.toUpperCase()));
}
return result;
}
}
/**
* Object converter for {@code boolean} values, because the construct
* {@link Boolean#Boolean(String)} is too lenient, anything that isn't "true"
* counts as {@code false}. Instead we want to be strict about boolean values
* like we were in the old data model.
*/
private static class BooleanConverter extends ObjectConverter<Boolean> {
@Override
public Boolean convertValue(String value, Class<? extends Boolean> datatype)
throws ParseException {
if (value == null) {
return null;
}
// NOTE(sven): "ture" is a hack for panasonic, they shipped cameras with
// this set before we became strict about boolean values. Photos could
// try to override this but it seemed easier to just allow it here.
if ("true".equals(value) || "1".equals(value) || "ture".equals(value)) {
return Boolean.TRUE;
} else if ("false".equals(value) || "0".equals(value)) {
return Boolean.FALSE;
} else {
throw new ParseException(
CoreErrorDomain.ERR.invalidBooleanAttribute.withInternalReason(
"Invalid boolean value: '" + value + "'"));
}
}
}
/**
* Object converter for pseudo-enum types backed by a map.
*/
public static class MappedEnumConverter<T> extends ObjectConverter<T> {
private final Map<String, T> map;
/**
* Creates a converter and links it with a map.
*
* @param map a map that converts string values into values of
* the correct type. The caller must make sure the map can
* be accessed concurrently before adding the resulting
* converter using {@link #addConverter}.
*/
public MappedEnumConverter(Map<String, T> map) {
this.map = map;
}
@Override
public T convertValue(String value, Class<? extends T> datatype)
throws ParseException {
T converted = map.get(value);
if (converted == null) {
throw new ParseException(
CoreErrorDomain.ERR.invalidEnumValue.withInternalReason(
"No such pseudo enum value of type " + datatype + " named "
+ value));
}
return converted;
}
}
}