blob: 0874bbc6dd238ca14212c330982d7bc69a8d587f [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.impl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.MultiValuesMap;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.FactoryMap;
import com.intellij.util.xml.*;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;
/**
* @author peter
*/
public class StaticGenericInfoBuilder {
private static final Set ADDER_PARAMETER_TYPES = new THashSet<Class>(Arrays.asList(Class.class, int.class));
private static final Logger LOG = Logger.getInstance(StaticGenericInfoBuilder.class);
private final Class myClass;
private final MultiValuesMap<XmlName, JavaMethod> myCollectionGetters = new MultiValuesMap<XmlName, JavaMethod>();
final MultiValuesMap<XmlName, JavaMethod> collectionAdders = new MultiValuesMap<XmlName, JavaMethod>();
final MultiValuesMap<XmlName, JavaMethod> collectionClassAdders = new MultiValuesMap<XmlName, JavaMethod>();
final MultiValuesMap<XmlName, JavaMethod> collectionIndexAdders = new MultiValuesMap<XmlName, JavaMethod>();
final MultiValuesMap<XmlName, JavaMethod> collectionIndexClassAdders = new MultiValuesMap<XmlName, JavaMethod>();
final MultiValuesMap<XmlName, JavaMethod> collectionClassIndexAdders = new MultiValuesMap<XmlName, JavaMethod>();
private final Map<XmlName, Type> myCollectionChildrenTypes = new THashMap<XmlName, Type>();
private final Map<JavaMethodSignature, String[]> myCompositeCollectionGetters = new THashMap<JavaMethodSignature, String[]>();
private final Map<JavaMethodSignature, Pair<String,String[]>> myCompositeCollectionAdders = new THashMap<JavaMethodSignature, Pair<String,String[]>>();
private final FactoryMap<XmlName, TIntObjectHashMap<Collection<JavaMethod>>> myFixedChildrenGetters = new FactoryMap<XmlName, TIntObjectHashMap<Collection<JavaMethod>>>() {
@Override
protected TIntObjectHashMap<Collection<JavaMethod>> create(final XmlName key) {
return new TIntObjectHashMap<Collection<JavaMethod>>();
}
};
private final Map<JavaMethodSignature, AttributeChildDescriptionImpl> myAttributes = new THashMap<JavaMethodSignature, AttributeChildDescriptionImpl>();
private boolean myValueElement;
private JavaMethod myNameValueGetter;
private JavaMethod myCustomChildrenGetter;
public StaticGenericInfoBuilder(final Class aClass) {
myClass = aClass;
final Set<JavaMethod> methods = new LinkedHashSet<JavaMethod>();
InvocationCache invocationCache = DomApplicationComponent.getInstance().getInvocationCache(myClass);
for (final Method method : ReflectionUtil.getClassPublicMethods(myClass)) {
methods.add(invocationCache.getInternedMethod(method));
}
for (final JavaMethod method : methods) {
if (DomImplUtil.isGetter(method) && method.getAnnotation(NameValue.class) != null) {
myNameValueGetter = method;
break;
}
}
{
final Class implClass = DomApplicationComponent.getInstance().getImplementation(myClass);
if (implClass != null) {
for (Method method : ReflectionUtil.getClassPublicMethods(implClass)) {
final int modifiers = method.getModifiers();
if (!Modifier.isAbstract(modifiers) &&
!Modifier.isVolatile(modifiers) &&
new JavaMethodSignature(method).findMethod(myClass) != null) {
methods.remove(invocationCache.getInternedMethod(method));
}
}
}
}
for (Iterator<JavaMethod> iterator = methods.iterator(); iterator.hasNext();) {
final JavaMethod method = iterator.next();
if (isCoreMethod(method) || DomImplUtil.isTagValueSetter(method) || method.getAnnotation(PropertyAccessor.class) != null) {
iterator.remove();
}
}
for (Iterator<JavaMethod> iterator = methods.iterator(); iterator.hasNext();) {
final JavaMethod method = iterator.next();
if (DomImplUtil.isGetter(method) && processGetterMethod(method)) {
iterator.remove();
}
}
for (Iterator<JavaMethod> iterator = methods.iterator(); iterator.hasNext();) {
final JavaMethod method = iterator.next();
final SubTagsList subTagsList = method.getAnnotation(SubTagsList.class);
if (subTagsList != null && method.getName().startsWith("add")) {
final String localName = subTagsList.tagName();
assert StringUtil.isNotEmpty(localName);
final String[] set = subTagsList.value();
assert Arrays.asList(set).contains(localName);
myCompositeCollectionAdders.put(method.getSignature(), Pair.create(localName, set));
iterator.remove();
}
else if (isAddMethod(method)) {
final XmlName xmlName = extractTagName(method, "add");
if (myCollectionGetters.containsKey(xmlName)) {
MultiValuesMap<XmlName, JavaMethod> adders = getAddersMap(method);
if (adders != null) {
adders.put(xmlName, method);
iterator.remove();
}
}
}
}
//noinspection ConstantIfStatement
if (false) {
if (!methods.isEmpty()) {
StringBuilder sb = new StringBuilder(myClass + " should provide the following implementations:");
for (JavaMethod method : methods) {
sb.append("\n ");
sb.append(method);
}
assert false : sb.toString();
//System.out.println(sb.toString());
}
}
}
@Nullable
private MultiValuesMap<XmlName, JavaMethod> getAddersMap(final JavaMethod method) {
final Class<?>[] parameterTypes = method.getParameterTypes();
switch (parameterTypes.length) {
case 0:
return collectionAdders;
case 1:
if (Class.class.equals(parameterTypes[0])) return collectionClassAdders;
if (isInt(parameterTypes[0])) return collectionIndexAdders;
break;
case 2:
if (isIndexClassAdder(parameterTypes[0], parameterTypes[1])) return collectionIndexClassAdders;
if (isIndexClassAdder(parameterTypes[1], parameterTypes[0])) return collectionClassIndexAdders;
}
return null;
}
private static boolean isIndexClassAdder(final Class<?> first, final Class<?> second) {
return isInt(first) && second.equals(Class.class);
}
private static boolean isInt(final Class<?> aClass) {
return aClass.equals(int.class) || aClass.equals(Integer.class);
}
private boolean isAddMethod(JavaMethod method) {
final XmlName tagName = extractTagName(method, "add");
if (tagName == null) return false;
final Type type = myCollectionChildrenTypes.get(tagName);
if (type == null || !ReflectionUtil.getRawType(type).isAssignableFrom(method.getReturnType())) return false;
return ADDER_PARAMETER_TYPES.containsAll(Arrays.asList(method.getParameterTypes()));
}
@Nullable
private XmlName extractTagName(JavaMethod method, @NonNls String prefix) {
final String name = method.getName();
if (!name.startsWith(prefix)) return null;
final SubTagList subTagAnnotation = method.getAnnotation(SubTagList.class);
if (subTagAnnotation != null && !StringUtil.isEmpty(subTagAnnotation.value())) {
return DomImplUtil.createXmlName(subTagAnnotation.value(), method);
}
final String tagName = getNameStrategy(false).convertName(name.substring(prefix.length()));
return StringUtil.isEmpty(tagName) ? null : DomImplUtil.createXmlName(tagName, method);
}
private static boolean isDomElement(final Type type) {
return type != null && DomElement.class.isAssignableFrom(ReflectionUtil.getRawType(type));
}
private boolean processGetterMethod(final JavaMethod method) {
if (DomImplUtil.isTagValueGetter(method)) {
myValueElement = true;
return true;
}
final Class returnType = method.getReturnType();
final boolean isAttributeValueMethod = GenericAttributeValue.class.isAssignableFrom(returnType);
final JavaMethodSignature signature = method.getSignature();
final Attribute annotation = method.getAnnotation(Attribute.class);
final boolean isAttributeMethod = annotation != null || isAttributeValueMethod;
if (annotation != null) {
assert
isAttributeValueMethod || GenericAttributeValue.class.isAssignableFrom(returnType) :
method + " should return GenericAttributeValue";
}
if (isAttributeMethod) {
final String s = annotation == null ? null : annotation.value();
String attributeName = StringUtil.isEmpty(s) ? getNameFromMethod(method, true) : s;
assert attributeName != null && StringUtil.isNotEmpty(attributeName) : "Can't guess attribute name from method name: " + method.getName();
final XmlName attrName = DomImplUtil.createXmlName(attributeName, method);
myAttributes.put(signature, new AttributeChildDescriptionImpl(attrName, method));
return true;
}
if (isDomElement(returnType)) {
final String qname = getSubTagName(method);
if (qname != null) {
final XmlName xmlName = DomImplUtil.createXmlName(qname, method);
Type collectionType = myCollectionChildrenTypes.get(xmlName);
if (collectionType != null) {
LOG.error("Collection (" + collectionType + ") and fixed children cannot intersect: " + qname + " for " + myClass);
}
int index = 0;
final SubTag subTagAnnotation = method.getAnnotation(SubTag.class);
if (subTagAnnotation != null && subTagAnnotation.index() != 0) {
index = subTagAnnotation.index();
}
final TIntObjectHashMap<Collection<JavaMethod>> map = myFixedChildrenGetters.get(xmlName);
Collection<JavaMethod> methods = map.get(index);
if (methods == null) {
map.put(index, methods = new SmartList<JavaMethod>());
}
methods.add(method);
return true;
}
}
final Type type = DomReflectionUtil.extractCollectionElementType(method.getGenericReturnType());
if (isDomElement(type)) {
final CustomChildren customChildren = method.getAnnotation(CustomChildren.class);
if (customChildren != null) {
myCustomChildrenGetter = method;
return true;
}
final SubTagsList subTagsList = method.getAnnotation(SubTagsList.class);
if (subTagsList != null) {
myCompositeCollectionGetters.put(signature, subTagsList.value());
return true;
}
final String qname = getSubTagNameForCollection(method);
if (qname != null) {
XmlName xmlName = DomImplUtil.createXmlName(qname, type, method);
assert !myFixedChildrenGetters.containsKey(xmlName) : "Collection and fixed children cannot intersect: " + qname;
myCollectionChildrenTypes.put(xmlName, type);
myCollectionGetters.put(xmlName, method);
return true;
}
}
return false;
}
private static boolean isCoreMethod(final JavaMethod method) {
if (method.getSignature().findMethod(DomElement.class) != null) return true;
final Class<?> aClass = method.getDeclaringClass();
return aClass.equals(GenericAttributeValue.class) || aClass.equals(GenericDomValue.class) && "getConverter".equals(method.getName());
}
@Nullable
private String getSubTagName(final JavaMethod method) {
final SubTag subTagAnnotation = method.getAnnotation(SubTag.class);
if (subTagAnnotation == null || StringUtil.isEmpty(subTagAnnotation.value())) {
return getNameFromMethod(method, false);
}
return subTagAnnotation.value();
}
@Nullable
private String getSubTagNameForCollection(final JavaMethod method) {
final SubTagList subTagList = method.getAnnotation(SubTagList.class);
if (subTagList == null || StringUtil.isEmpty(subTagList.value())) {
final String propertyName = getPropertyName(method);
if (propertyName != null) {
final String singular = StringUtil.unpluralize(propertyName);
assert singular != null : "Can't unpluralize: " + propertyName;
return getNameStrategy(false).convertName(singular);
}
else {
return null;
}
}
return subTagList.value();
}
@Nullable
private String getNameFromMethod(final JavaMethod method, boolean isAttribute) {
final String propertyName = getPropertyName(method);
return propertyName == null ? null : getNameStrategy(isAttribute).convertName(propertyName);
}
@Nullable
private static String getPropertyName(JavaMethod method) {
return StringUtil.getPropertyName(method.getMethodName());
}
@NotNull
private DomNameStrategy getNameStrategy(boolean isAttribute) {
final DomNameStrategy strategy = DomImplUtil.getDomNameStrategy(ReflectionUtil.getRawType(myClass), isAttribute);
return strategy != null ? strategy : DomNameStrategy.HYPHEN_STRATEGY;
}
final JavaMethod getCustomChildrenGetter() {
return myCustomChildrenGetter;
}
final Map<JavaMethodSignature, AttributeChildDescriptionImpl> getAttributes() {
return myAttributes;
}
final Map<JavaMethodSignature, Pair<FixedChildDescriptionImpl, Integer>> getFixedGetters() {
final Map<JavaMethodSignature, Pair<FixedChildDescriptionImpl, Integer>> map = new THashMap<JavaMethodSignature, Pair<FixedChildDescriptionImpl, Integer>>();
final Set<XmlName> names = myFixedChildrenGetters.keySet();
for (final XmlName name : names) {
final TIntObjectHashMap<Collection<JavaMethod>> map1 = myFixedChildrenGetters.get(name);
int max = 0;
final int[] ints = map1.keys();
for (final int i : ints) {
max = Math.max(max, i);
}
int count = max + 1;
final Collection<JavaMethod>[] getters = new Collection[count];
for (final int i : ints) {
getters[i] = map1.get(i);
}
final FixedChildDescriptionImpl description = new FixedChildDescriptionImpl(name, map1.get(0).iterator().next().getGenericReturnType(), count, getters);
for (int i = 0; i < getters.length; i++) {
final Collection<JavaMethod> collection = getters[i];
for (final JavaMethod method : collection) {
map.put(method.getSignature(), Pair.create(description, i));
}
}
}
return map;
}
final Map<JavaMethodSignature, CollectionChildDescriptionImpl> getCollectionGetters() {
final Map<JavaMethodSignature, CollectionChildDescriptionImpl> getters = new THashMap<JavaMethodSignature, CollectionChildDescriptionImpl>();
for (final XmlName xmlName : myCollectionGetters.keySet()) {
final Collection<JavaMethod> collGetters = myCollectionGetters.get(xmlName);
final JavaMethod method = collGetters.iterator().next();
final CollectionChildDescriptionImpl description = new CollectionChildDescriptionImpl(xmlName, DomReflectionUtil.extractCollectionElementType(method.getGenericReturnType()),
collGetters
);
for (final JavaMethod getter : collGetters) {
getters.put(getter.getSignature(), description);
}
}
return getters;
}
final Map<JavaMethodSignature, Pair<String, String[]>> getCompositeCollectionAdders() {
return myCompositeCollectionAdders;
}
final Map<JavaMethodSignature, String[]> getCompositeCollectionGetters() {
return myCompositeCollectionGetters;
}
public JavaMethod getNameValueGetter() {
return myNameValueGetter;
}
public boolean isValueElement() {
return myValueElement;
}
}