/*
* Copyright 2011 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.gradle.internal.typeconversion;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.tasks.Optional;
import org.gradle.internal.UncheckedException;
import org.gradle.internal.exceptions.DiagnosticsVisitor;
import org.gradle.internal.reflect.ReflectionCache;
import org.gradle.util.ConfigureUtil;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Converts a {@code Map<String, Object>} to the target type. Subclasses should define a {@code T parseMap()} method which takes a parameter
* for each key value required from the source map. Each parameter should be annotated with a {@code @MapKey} annotation, and can also
* be annotated with a {@code @optional} annotation.
*/
public abstract class MapNotationConverter<T> extends TypedNotationConverter<Map, T> {
public MapNotationConverter() {
super(Map.class);
}
@Override
public void describe(DiagnosticsVisitor visitor) {
visitor.candidate("Maps");
}
public T parseType(Map values) throws UnsupportedNotationException {
Map<String, Object> mutableValues = new HashMap<String, Object>(values);
Set<String> missing = null;
ConvertMethod convertMethod = null;
Method method = null;
while (method == null) {
convertMethod = ConvertMethod.of(this.getClass());
// since we need access to the method and that it's weakly referenced
// we always need to double check that it hasn't been collected
method = convertMethod.getMethod();
}
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] params = new Object[parameterTypes.length];
String[] keyNames = convertMethod.keyNames;
boolean[] optionals = convertMethod.optional;
for (int i = 0; i < params.length; i++) {
String keyName = keyNames[i];
boolean optional = optionals[i];
Class<?> type = parameterTypes[i];
Object value;
if (type == String.class) {
value = get(mutableValues, keyName);
} else {
value = type.cast(mutableValues.get(keyName));
}
if (!optional && value == null) {
if (missing == null) {
missing = new TreeSet<String>();
}
missing.add(keyName);
}
mutableValues.remove(keyName);
params[i] = value;
}
if (missing != null) {
//below could be better.
//Throwing InvalidUserDataException here means that useful context information (including candidate formats, etc.) is not presented to the user
throw new InvalidUserDataException(String.format("Required keys %s are missing from map %s.", missing, values));
}
T result;
try {
result = (T) method.invoke(this, params);
} catch (IllegalAccessException e) {
throw UncheckedException.throwAsUncheckedException(e);
} catch (InvocationTargetException e) {
throw UncheckedException.unwrapAndRethrow(e);
}
ConfigureUtil.configureByMap(mutableValues, result);
return result;
}
protected String get(Map<String, Object> args, String key) {
Object value = args.get(key);
String str = value != null ? value.toString() : null;
if (str != null && str.length() == 0) {
return null;
}
return str;
}
private static class ConvertMethodCache extends ReflectionCache<ConvertMethod> {
@Override
protected ConvertMethod create(Class<?> key, Class<?>[] params) {
Method convertMethod = findConvertMethod(key);
Annotation[][] parameterAnnotations = convertMethod.getParameterAnnotations();
String[] keyNames = new String[parameterAnnotations.length];
boolean[] optional = new boolean[parameterAnnotations.length];
for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] annotations = parameterAnnotations[i];
keyNames[i] = keyName(annotations);
optional[i] = optional(annotations);
}
return new ConvertMethod(convertMethod, keyNames, optional);
}
private static Method findConvertMethod(Class clazz) {
for (Method method : clazz.getDeclaredMethods()) {
if (method.getName().equals("parseMap")) {
method.setAccessible(true);
return method;
}
}
throw new UnsupportedOperationException(String.format("No parseMap() method found on class %s.", clazz.getSimpleName()));
}
private static boolean optional(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof Optional) {
return true;
}
}
return false;
}
private static String keyName(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof MapKey) {
return ((MapKey) annotation).value();
}
}
throw new UnsupportedOperationException("No @Key annotation on parameter of parseMap() method");
}
}
private static class ConvertMethod extends ReflectionCache.CachedInvokable<Method> {
private final static ConvertMethodCache CONVERT_METHODS = new ConvertMethodCache();
public static final Class[] EMPTY = new Class[0];
private final String[] keyNames;
private final boolean[] optional;
private ConvertMethod(Method method, String[] keyNames, boolean[] optional) {
super(method);
this.keyNames = keyNames;
this.optional = optional;
}
public static synchronized ConvertMethod of(Class clazz) {
return CONVERT_METHODS.get(clazz, EMPTY);
}
}
}