blob: 6d6e95c4c8a95e5034bf27c042bca2b42cf3d668 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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.intellij.util.xml;
import com.intellij.openapi.util.Pair;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ConcurrentFactoryMap;
import com.intellij.util.containers.FactoryMap;
import com.intellij.util.xml.impl.DomInvocationHandler;
import com.intellij.util.xml.impl.DomManagerImpl;
import com.intellij.util.xml.reflect.AbstractDomChildrenDescription;
import gnu.trove.THashSet;
import net.sf.cglib.proxy.AdvancedProxy;
import net.sf.cglib.proxy.InvocationHandler;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;
/**
* @author peter
*/
public class ModelMergerImpl implements ModelMerger {
private final List<Pair<InvocationStrategy,Class>> myInvocationStrategies = new ArrayList<Pair<InvocationStrategy,Class>>();
private final List<MergingStrategy> myMergingStrategies = new ArrayList<MergingStrategy>();
private final List<Class> myMergingStrategyClasses = new ArrayList<Class>();
private static final Class<MergedObject> MERGED_OBJECT_CLASS = MergedObject.class;
private final ConcurrentFactoryMap<Method,List<Pair<InvocationStrategy,Class>>> myAcceptsCache = new ConcurrentFactoryMap<Method,List<Pair<InvocationStrategy,Class>>>() {
protected List<Pair<InvocationStrategy,Class>> create(final Method method) {
List<Pair<InvocationStrategy,Class>> result = new ArrayList<Pair<InvocationStrategy,Class>>();
for (int i = myInvocationStrategies.size() - 1; i >= 0; i--) {
final Pair<InvocationStrategy, Class> pair = myInvocationStrategies.get(i);
if (pair.first.accepts(method)) {
result.add(pair);
}
}
return result;
}
};
public ModelMergerImpl() {
addInvocationStrategy(Object.class, new InvocationStrategy<Object>() {
public boolean accepts(final Method method) {
return true;
}
public Object invokeMethod(final JavaMethod javaMethod, final Object proxy, final Object[] args, final List<Object> implementations)
throws IllegalAccessException, InvocationTargetException {
final Method method = javaMethod.getMethod();
List<Object> results = getMergedImplementations(method, args, method.getReturnType(), implementations, isIntersectionMethod(javaMethod));
return results.isEmpty() ? null : results.get(0);
}
});
addInvocationStrategy(Object.class, new InvocationStrategy<Object>() {
public boolean accepts(final Method method) {
return Collection.class.isAssignableFrom(method.getReturnType());
}
public Object invokeMethod(final JavaMethod method, final Object proxy, final Object[] args, final List<Object> implementations)
throws IllegalAccessException, InvocationTargetException {
final Type type = DomReflectionUtil.extractCollectionElementType(method.getGenericReturnType());
assert type != null : "No generic return type in method " + method;
return getMergedImplementations(method.getMethod(), args, ReflectionUtil.getRawType(type), implementations, isIntersectionMethod(method));
}
});
addInvocationStrategy(Object.class, new InvocationStrategy<Object>() {
public boolean accepts(final Method method) {
return Object.class.equals(method.getDeclaringClass());
}
public Object invokeMethod(final JavaMethod method, final Object proxy, final Object[] args, final List<Object> implementations) {
@NonNls String methodName = method.getName();
if ("toString".equals(methodName)) {
return "Merger: " + implementations;
}
if ("hashCode".equals(methodName)) {
int result = 1;
for (Object element : implementations)
result = 31 * result + element.hashCode();
return result;
}
if ("equals".equals(methodName)) {
final Object arg = args[0];
return arg != null && arg instanceof MergedObject && implementations.equals(((MergedObject)arg).getImplementations());
}
return null;
}
});
addInvocationStrategy(Object.class, new InvocationStrategy<Object>() {
public boolean accepts(final Method method) {
return "isValid".equals(method.getName());
}
public Object invokeMethod(final JavaMethod method, final Object proxy, final Object[] args, final List<Object> implementations)
throws IllegalAccessException, InvocationTargetException {
for (final Object implementation : implementations) {
if (!((Boolean)method.invoke(implementation, args))) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
});
addInvocationStrategy(Object.class, new InvocationStrategy<Object>() {
public boolean accepts(final Method method) {
return void.class.equals(method.getReturnType());
}
public Object invokeMethod(final JavaMethod method, final Object proxy, final Object[] args, final List<Object> implementations)
throws IllegalAccessException, InvocationTargetException {
for (final Object t : implementations) {
method.invoke(t, args);
}
return null;
}
});
addInvocationStrategy(Object.class, new InvocationStrategy<Object>() {
public boolean accepts(final Method method) {
return MERGED_OBJECT_CLASS.equals(method.getDeclaringClass());
}
public Object invokeMethod(final JavaMethod method, final Object proxy, final Object[] args, final List<Object> implementations)
throws IllegalAccessException, InvocationTargetException {
assert "getImplementations".equals(method.getName());
return implementations;
}
});
addInvocationStrategy(DomElement.class, new InvocationStrategy<DomElement>() {
public boolean accepts(final Method method) {
return DomInvocationHandler.ACCEPT_METHOD.equals(method);
}
public Object invokeMethod(final JavaMethod method, final DomElement proxy, final Object[] args, final List<DomElement> implementations)
throws IllegalAccessException, InvocationTargetException {
final DomElementVisitor visitor = (DomElementVisitor)args[0];
((DomManagerImpl)implementations.get(0).getManager()).getApplicationComponent().getVisitorDescription(visitor.getClass()).acceptElement(visitor, proxy);
return null;
}
});
addInvocationStrategy(DomElement.class, new InvocationStrategy<DomElement>() {
public boolean accepts(final Method method) {
return DomInvocationHandler.ACCEPT_CHILDREN_METHOD.equals(method);
}
public Object invokeMethod(final JavaMethod method, final DomElement proxy, final Object[] args, final List<DomElement> implementations)
throws IllegalAccessException, InvocationTargetException {
final DomElementVisitor visitor = (DomElementVisitor)args[0];
for (final AbstractDomChildrenDescription description : implementations.get(0).getGenericInfo().getChildrenDescriptions()) {
for (final DomElement value : description.getValues(proxy)) {
value.accept(visitor);
}
}
return null;
}
});
}
private boolean isIntersectionMethod(final JavaMethod javaMethod) {
return javaMethod.getMethod().getAnnotation(Intersect.class) != null;
}
public final <T> void addInvocationStrategy(Class<T> aClass, InvocationStrategy<T> strategy) {
myInvocationStrategies.add(Pair.<InvocationStrategy,Class>create(strategy, aClass));
}
public final <T> void addMergingStrategy(Class<T> aClass, MergingStrategy<T> strategy) {
myMergingStrategies.add(strategy);
myMergingStrategyClasses.add(aClass);
}
public <T> T mergeModels(final Class<T> aClass, final T... implementations) {
if (implementations.length == 1) return implementations[0];
final MergingInvocationHandler<T> handler = new MergingInvocationHandler<T>(aClass, Arrays.asList(implementations));
return _mergeModels(aClass, handler, implementations);
}
public <T> T mergeModels(final Class<T> aClass, final Collection<? extends T> implementations) {
return (T)mergeModels((Class)aClass, implementations.toArray());
}
private <T> T _mergeModels(final Class<? super T> aClass, final MergingInvocationHandler<T> handler, final T... implementations) {
final Set<Class> commonClasses = getCommonClasses(new THashSet<Class>(), implementations);
commonClasses.add(MERGED_OBJECT_CLASS);
commonClasses.add(aClass);
final T t = AdvancedProxy.<T>createProxy(handler, null, commonClasses.toArray(new Class[commonClasses.size()]));
return t;
}
private static <T extends Collection<Class>> T getCommonClasses(final T result, final Object... implementations) {
if (implementations.length > 0) {
DomUtil.getAllInterfaces(implementations[0].getClass(), result);
for (int i = 1; i < implementations.length; i++) {
final ArrayList<Class> list1 = new ArrayList<Class>();
DomUtil.getAllInterfaces(implementations[i].getClass(), list1);
result.retainAll(list1);
}
}
return result;
}
private static final Map<Class<? extends Object>, Method> ourPrimaryKeyMethods = new HashMap<Class<? extends Object>, Method>();
public class MergingInvocationHandler<T> implements InvocationHandler {
private final Class<? super T> myClass;
private List<T> myImplementations;
public MergingInvocationHandler(final Class<T> aClass, final List<T> implementations) {
this(aClass);
for (final T implementation : implementations) {
if (implementation instanceof StableElement) {
throw new AssertionError("Stable values merging is prohibited: " + implementation);
}
}
myImplementations = implementations;
}
public MergingInvocationHandler(final Class<T> aClass) {
myClass = aClass;
}
@NotNull
private InvocationStrategy findStrategy(final Object proxy, final Method method) {
for (final Pair<InvocationStrategy, Class> pair : myAcceptsCache.get(method)) {
if (Object.class.equals(pair.second) || pair.second.isInstance(proxy)) {
return pair.first;
}
}
throw new AssertionError("impossible");
}
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
try {
return findStrategy(proxy, method).invokeMethod(getJavaMethod(method), proxy, args, myImplementations);
}
catch (InvocationTargetException e) {
throw e.getCause();
}
}
private JavaMethod getJavaMethod(final Method method) {
if (ReflectionUtil.isAssignable(MERGED_OBJECT_CLASS, method.getDeclaringClass())) {
return JavaMethod.getMethod(MERGED_OBJECT_CLASS, method);
}
if (ReflectionUtil.isAssignable(method.getDeclaringClass(), myClass)) {
return JavaMethod.getMethod(myClass, method);
}
return JavaMethod.getMethod(method.getDeclaringClass(), method);
}
}
@Nullable
private static Object getPrimaryKey(Object implementation, final boolean singleValuedInvocation) {
final Method method = getPrimaryKeyMethod(implementation.getClass());
if (method != null) {
final Object o = DomReflectionUtil.invokeMethod(method, implementation);
return ReflectionUtil.isAssignable(GenericValue.class, method.getReturnType()) ? ((GenericValue)o).getValue() : o;
}
else {
if (implementation instanceof GenericValue) {
return singleValuedInvocation? Boolean.TRUE : ((GenericValue)implementation).getValue();
}
else {
return null;
}
}
}
@Nullable
private static Method getPrimaryKeyMethod(final Class<? extends Object> aClass) {
Method method = ourPrimaryKeyMethods.get(aClass);
if (method == null) {
if (ourPrimaryKeyMethods.containsKey(aClass)) return null;
for (final Method method1 : ReflectionUtil.getClassPublicMethods(aClass)) {
if ((method = findPrimaryKeyAnnotatedMethod(method1, aClass)) != null) {
break;
}
}
ourPrimaryKeyMethods.put(aClass, method);
}
return method;
}
@Nullable
private static Method findPrimaryKeyAnnotatedMethod(final Method method, final Class aClass) {
return method.getReturnType() != void.class && method.getParameterTypes().length == 0 ? new JavaMethodSignature(method)
.findAnnotatedMethod(PrimaryKey.class, aClass) : null;
}
private List<Object> getMergedImplementations(final Method method,
final Object[] args,
final Class returnType,
final List<Object> implementations,
final boolean intersect) throws IllegalAccessException, InvocationTargetException {
final List<Object> results = new ArrayList<Object>();
if (returnType.isInterface()) {
final List<Object> orderedPrimaryKeys = new SmartList<Object>();
final FactoryMap<Object, List<Set<Object>>> map = new FactoryMap<Object, List<Set<Object>>>() {
@NotNull
protected List<Set<Object>> create(final Object key) {
orderedPrimaryKeys.add(key);
return new SmartList<Set<Object>>();
}
};
final FactoryMap<Object, int[]> counts = new FactoryMap<Object, int[]>() {
@NotNull
protected int[] create(final Object key) {
return new int[implementations.size()];
}
};
for (int i = 0; i < implementations.size(); i++) {
Object t = implementations.get(i);
final Object o = method.invoke(t, args);
if (o instanceof Collection) {
for (final Object o1 : (Collection)o) {
addToMaps(o1, counts, map, i, results, false, intersect);
}
}
else if (o != null) {
addToMaps(o, counts, map, i, results, true, intersect);
}
}
for (final Object primaryKey : orderedPrimaryKeys) {
for (final Set<Object> objects : map.get(primaryKey)) {
results.add(mergeImplementations(returnType, new ArrayList<Object>(objects)));
}
}
}
else {
HashSet<Object> map = new HashSet<Object>();
for (final Object t : implementations) {
final Object o = method.invoke(t, args);
if (o instanceof Collection) {
map.addAll((Collection<Object>)o);
}
else if (o != null) {
map.add(o);
break;
}
}
results.addAll(map);
}
return results;
}
protected final Object mergeImplementations(final Class returnType, final List<Object> implementations) {
for (int i = myMergingStrategies.size() - 1; i >= 0; i--) {
if (ReflectionUtil.isAssignable(myMergingStrategyClasses.get(i), returnType)) {
final Object o = myMergingStrategies.get(i).mergeChildren(returnType, implementations);
if (o != null) {
return o;
}
}
}
if (implementations.size() == 1) {
return implementations.get(0);
}
return mergeModels(returnType, implementations);
}
private boolean addToMaps(final Object o,
final FactoryMap<Object, int[]> counts,
final FactoryMap<Object, List<Set<Object>>> map,
final int index,
final List<Object> results,
final boolean singleValuedInvocation,
final boolean intersect) throws IllegalAccessException, InvocationTargetException {
final Object primaryKey = getPrimaryKey(o, singleValuedInvocation);
if (primaryKey != null || singleValuedInvocation) {
final List<Set<Object>> list = map.get(primaryKey);
final int[] indices = counts.get(primaryKey);
int objIndex = intersect? indices[index] : indices[index]++;
if (list.size() <= objIndex) {
list.add(new LinkedHashSet<Object>());
}
list.get(objIndex).add(o);
return false;
}
results.add(o);
return true;
}
}