blob: 22048e507a7740339000435b0a9b1aa60b32b0e1 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.idea.gradle.util;
import com.android.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.gradle.tooling.model.UnsupportedMethodException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.Serializable;
import java.lang.reflect.*;
import java.util.*;
public final class ProxyUtil {
/*
* List of generic classes supported by the reproxy method. When the reproxy method is updated to handle more classes,
* this list needs to be updated with those additional classes and when this class is used reproxy any class the
* ProxyUtilTest#testSupportedTypes needs to be updated to keep checking the applicability of reproxying to that class.
*/
@SuppressWarnings("unchecked") private static final Set<Class<?>> SUPPORTED_TYPES =
ImmutableSet.of(File.class, Boolean.class, String.class, Integer.class, Collection.class, Set.class, List.class, Map.class);
private ProxyUtil() {
}
@VisibleForTesting
static boolean isSupported(@NotNull Class<?> clazz) {
return clazz.isPrimitive() || SUPPORTED_TYPES.contains(clazz);
}
/**
* Regenerate proxy objects with a serializable version of a proxy.
* This method is intended to be run on objects that are a bag of properties, particularly custom Gradle model objects.
* Here we assume that the given object can be represented as a map of method name to return value. The original object
* is regenerated using this assumption which gives a serializable/deserializable object.
*
* If a method throws an exception it is assumed that it is the intended behaviour and the same exception will be thrown in the
* reproxied counterpart. This is useful for Gradle model methods that are not present in the actual model object being used.
*
* @param object the object to 'reproxy'.
* @param type the runtime type of the object. This is the expected type of object, and must be a superclass or equals to T.
* @param <T> the type of the object.
* @return the reproxied object.
*/
@SuppressWarnings("unchecked")
@Nullable
public static <T> T reproxy(@NotNull Type type, @Nullable T object) {
if (object == null) {
return null;
}
if (object instanceof InvocationErrorValue) {
return object;
}
if (type instanceof ParameterizedType) {
ParameterizedType genericType = (ParameterizedType)type;
if (genericType.getRawType() instanceof Class) {
Class<?> genericClass = (Class<?>)genericType.getRawType();
if (Collection.class.isAssignableFrom(genericClass)) {
Collection<Object> collection = (Collection<Object>)object;
Collection<Object> newCollection;
if (genericClass.isAssignableFrom(ArrayList.class)) {
newCollection = Lists.newArrayListWithCapacity(collection.size());
}
else if (genericClass.isAssignableFrom(LinkedHashSet.class)) {
newCollection = Sets.newLinkedHashSet();
}
else {
throw new IllegalStateException("Unsupported collection type: " + genericClass.getCanonicalName());
}
Type argument = genericType.getActualTypeArguments()[0];
for (Object item : collection) {
newCollection.add(reproxy(argument, item));
}
return (T)newCollection;
}
else if (Map.class.isAssignableFrom(genericClass)) {
Map<Object, Object> map = (Map<Object, Object>)object;
Map<Object, Object> newMap = Maps.newLinkedHashMap();
Type keyType = genericType.getActualTypeArguments()[0];
Type valueType = genericType.getActualTypeArguments()[1];
for (Map.Entry entry : map.entrySet()) {
newMap.put(reproxy(keyType, entry.getKey()), reproxy(valueType, entry.getValue()));
}
return (T)newMap;
}
else {
throw new IllegalStateException("Unsupported generic type: " + genericClass.getCanonicalName());
}
}
else {
throw new IllegalStateException("Unsupported raw type.");
}
}
// Only modify proxy objects...
if (!Proxy.isProxyClass(object.getClass())) {
return object;
}
// ...that are not our own proxy.
if (Proxy.getInvocationHandler(object) instanceof WrapperInvocationHandler) {
return object;
}
Class<?>[] interfaces = object.getClass().getInterfaces();
if (interfaces.length != 1) {
throw new IllegalStateException("Cannot 'reproxy' a class with multiple interfaces");
}
Class<?> clazz = interfaces[0];
final Map<String, Object> values = Maps.newHashMap();
for (Method m : clazz.getMethods()) {
try {
if (Modifier.isPublic(m.getModifiers())) {
Object value;
try {
value = m.invoke(object);
} catch (InvocationTargetException e) {
value = new InvocationErrorValue(e.getCause());
}
values.put(m.toGenericString(), reproxy(m.getGenericReturnType(), value));
}
}
catch (IllegalAccessException e) {
throw new IllegalStateException("A non public method shouldn't have been called.", e);
}
}
return (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new WrapperInvocationHandler(values));
}
static class WrapperInvocationHandler implements InvocationHandler, Serializable {
private static final Method TO_STRING = getObjectMethod("toString");
private static final Method HASHCODE = getObjectMethod("hashCode");
private static final Method EQUALS = getObjectMethod("equals", Object.class);
@VisibleForTesting
final Map<String, Object> values;
WrapperInvocationHandler(@NotNull Map<String, Object> values) {
this.values = values;
}
@NotNull
private static Method getObjectMethod(@NotNull String name, @NotNull Class<?>... types) {
try {
return Object.class.getMethod(name, types);
}
catch (NoSuchMethodException e) {
throw new IllegalStateException("Method should exist in Object", e);
}
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
if (method.equals(TO_STRING) || method.equals(HASHCODE)) {
return method.invoke(this, objects);
}
else if (method.equals(EQUALS)) {
return proxyEquals(objects[0]);
}
else {
String key = method.toGenericString();
if (!values.containsKey(key)) {
throw new UnsupportedMethodException("Method " + key + " not found");
}
Object value = values.get(key);
if (value instanceof InvocationErrorValue) {
throw ((InvocationErrorValue)value).exception;
}
return value;
}
}
private boolean proxyEquals(Object other) {
return other != null && Proxy.isProxyClass(other.getClass()) && Proxy.getInvocationHandler(other).equals(this);
}
}
private static class InvocationErrorValue implements Serializable {
public Throwable exception;
private InvocationErrorValue(Throwable exception) {
this.exception = exception;
}
}
}