/*
* Copyright 2014-2017 the original author or authors.
*
* 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 org.glowroot.ui;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
import com.google.common.base.CaseFormat;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import static com.google.common.base.Preconditions.checkNotNull;
class QueryStrings {
private static LoadingCache<Class<?>, Map<String, Method>> settersCache =
CacheBuilder.newBuilder().build(new SettersCacheBuilder());
private QueryStrings() {}
static <T> /*@NonNull*/ T decode(Map<String, List<String>> queryParameters, Class<T> clazz)
throws Exception {
Class<?> immutableClass = getImmutableClass(clazz);
Method builderMethod = immutableClass.getDeclaredMethod("builder");
Object builder = builderMethod.invoke(null);
checkNotNull(builder);
Class<?> immutableBuilderClass = builder.getClass();
Map<String, Method> setters = settersCache.getUnchecked(immutableBuilderClass);
for (Entry<String, List<String>> entry : queryParameters.entrySet()) {
String key = entry.getKey();
key = CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, key);
// special rule for "-mbean" so that it will convert to "...MBean"
key = key.replace("Mbean", "MBean");
Method setter = setters.get(key);
checkNotNull(setter, "Unexpected attribute: %s", key);
Type valueType = setter.getGenericParameterTypes()[0];
Object value;
if (valueType instanceof ParameterizedType) {
// only generic iterable supported
valueType = ((ParameterizedType) valueType).getActualTypeArguments()[0];
List<Object> parsedValues = Lists.newArrayList();
for (String stringValue : entry.getValue()) {
Object parsedValue = parseString(stringValue, (Class<?>) valueType);
// ignore empty query param values, e.g. the empty percentile value in
// percentile=&percentile=95&percentile=99
if (parsedValue != null) {
parsedValues.add(parsedValue);
}
}
value = parsedValues;
} else {
value = parseString(entry.getValue().get(0), (Class<?>) valueType);
}
setter.invoke(builder, value);
}
Method build = immutableBuilderClass.getDeclaredMethod("build");
@SuppressWarnings("unchecked")
T decoded = (T) build.invoke(builder);
return checkNotNull(decoded);
}
static <T> Class<?> getImmutableClass(Class<T> clazz) throws ClassNotFoundException {
String prefix = "";
Package pkg = clazz.getPackage();
if (pkg != null) {
prefix = pkg.getName() + '.';
}
String immutableClassName = prefix + "Immutable" + clazz.getSimpleName();
Class<?> immutableClass = Class.forName(immutableClassName, false, clazz.getClassLoader());
return immutableClass;
}
private static @Nullable Object parseString(String str, Class<?> targetClass) {
if (targetClass == String.class) {
return str;
} else if (isInteger(targetClass)) {
// parse as double and truncate, just in case there is a decimal part
return (int) Double.parseDouble(str);
} else if (isLong(targetClass)) {
// parse as double and truncate, just in case there is a decimal part
return (long) Double.parseDouble(str);
} else if (isDouble(targetClass)) {
return Double.parseDouble(str);
} else if (isBoolean(targetClass)) {
return Boolean.parseBoolean(str);
} else if (Enum.class.isAssignableFrom(targetClass)) {
@SuppressWarnings({"unchecked", "rawtypes"})
Enum<?> enumValue = Enum.valueOf((Class<? extends Enum>) targetClass,
str.replace('-', '_').toUpperCase(Locale.ENGLISH));
return enumValue;
} else {
throw new IllegalStateException("Unexpected class: " + targetClass);
}
}
private static boolean isInteger(Class<?> targetClass) {
return targetClass == int.class || targetClass == Integer.class;
}
private static boolean isLong(Class<?> targetClass) {
return targetClass == long.class || targetClass == Long.class;
}
private static boolean isDouble(Class<?> targetClass) {
return targetClass == double.class || targetClass == Double.class;
}
private static boolean isBoolean(Class<?> targetClass) {
return targetClass == boolean.class || targetClass == Boolean.class;
}
private static class SettersCacheBuilder extends CacheLoader<Class<?>, Map<String, Method>> {
@Override
public Map<String, Method> load(Class<?> key) throws Exception {
Map<String, Method> setters = Maps.newHashMap();
for (Method method : key.getMethods()) {
if (method.getName().startsWith("add") && !method.getName().startsWith("addAll")) {
continue;
}
if (method.getParameterTypes().length == 1) {
if (!isSimpleSetter(method.getParameterTypes()[0])) {
continue;
}
method.setAccessible(true);
if (method.getName().startsWith("addAll")) {
String propertyName = method.getName().substring(6);
propertyName = Character.toLowerCase(propertyName.charAt(0))
+ propertyName.substring(1);
setters.put(propertyName, method);
} else {
setters.put(method.getName(), method);
}
}
}
return setters;
}
private static boolean isSimpleSetter(Class<?> targetClass) {
return targetClass == String.class
|| isInteger(targetClass)
|| isLong(targetClass)
|| isDouble(targetClass)
|| isBoolean(targetClass)
|| Enum.class.isAssignableFrom(targetClass)
|| targetClass == Iterable.class;
}
}
}