blob: 723568d2ebebf8a156f690e947e662093fb3759a [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.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ConcurrentFactoryMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.xml.reflect.*;
import com.intellij.xml.util.XmlTagUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.*;
/**
* @author peter
*/
public class DomUtil {
private static final Logger LOG = Logger.getInstance("#com.intellij.util.xml.DomUtil");
public static final TypeVariable<Class<GenericValue>> GENERIC_VALUE_TYPE_VARIABLE = GenericValue.class.getTypeParameters()[0];
private static final Class<Void> DUMMY = void.class;
private static final Key<DomFileElement> FILE_ELEMENT_KEY = Key.create("dom file element");
private static final ConcurrentFactoryMap<Type, Class> ourTypeParameters = new ConcurrentFactoryMap<Type, Class>() {
@Override
@NotNull
protected Class create(final Type key) {
final Class<?> result = substituteGenericType(GENERIC_VALUE_TYPE_VARIABLE, key);
return result == null ? DUMMY : result;
}
};
private static final ConcurrentFactoryMap<Couple<Type>, Class> ourVariableSubstitutions = new ConcurrentFactoryMap<Couple<Type>, Class>() {
@Override
@Nullable
protected Class create(final Couple<Type> key) {
return ReflectionUtil.substituteGenericType(key.first, key.second);
}
};
public static Class extractParameterClassFromGenericType(Type type) {
return getGenericValueParameter(type);
}
public static boolean isGenericValueType(Type type) {
return getGenericValueParameter(type) != null;
}
@Nullable
public static <T extends DomElement> T findByName(@NotNull Collection<T> list, @NonNls @NotNull String name) {
for (T element: list) {
String elementName = element.getGenericInfo().getElementName(element);
if (elementName != null && elementName.equals(name)) {
return element;
}
}
return null;
}
@NotNull
public static String[] getElementNames(@NotNull Collection<? extends DomElement> list) {
ArrayList<String> result = new ArrayList<String>(list.size());
if (list.size() > 0) {
for (DomElement element: list) {
String name = element.getGenericInfo().getElementName(element);
if (name != null) {
result.add(name);
}
}
}
return ArrayUtil.toStringArray(result);
}
@NotNull
public static List<XmlTag> getElementTags(@NotNull Collection<? extends DomElement> list) {
ArrayList<XmlTag> result = new ArrayList<XmlTag>(list.size());
for (DomElement element: list) {
XmlTag tag = element.getXmlTag();
if (tag != null) {
result.add(tag);
}
}
return result;
}
@NotNull
public static XmlTag[] getElementTags(@NotNull DomElement[] list) {
XmlTag[] result = new XmlTag[list.length];
int i = 0;
for (DomElement element: list) {
XmlTag tag = element.getXmlTag();
if (tag != null) {
result[i++] = tag;
}
}
return result;
}
@Nullable
public static List<JavaMethod> getFixedPath(DomElement element) {
assert element.isValid();
final LinkedList<JavaMethod> methods = new LinkedList<JavaMethod>();
while (true) {
final DomElement parent = element.getParent();
if (parent instanceof DomFileElement) {
break;
}
final JavaMethod method = getGetterMethod(element, parent);
if (method == null) {
return null;
}
methods.addFirst(method);
element = element.getParent();
}
return methods;
}
@Nullable
private static JavaMethod getGetterMethod(final DomElement element, final DomElement parent) {
final String xmlElementName = element.getXmlElementName();
final String namespace = element.getXmlElementNamespaceKey();
final DomGenericInfo genericInfo = parent.getGenericInfo();
if (element instanceof GenericAttributeValue) {
final DomAttributeChildDescription description = genericInfo.getAttributeChildDescription(xmlElementName, namespace);
assert description != null;
return description.getGetterMethod();
}
final DomFixedChildDescription description = genericInfo.getFixedChildDescription(xmlElementName, namespace);
return description != null ? description.getGetterMethod(description.getValues(parent).indexOf(element)) : null;
}
public static Class<?> substituteGenericType(Type genericType, Type classType) {
return ourVariableSubstitutions.get(Couple.of(genericType, classType));
}
@Nullable
public static Class getGenericValueParameter(Type type) {
final Class aClass = ourTypeParameters.get(type);
return aClass == DUMMY ? null : aClass;
}
@Nullable
public static XmlElement getValueElement(GenericDomValue domValue) {
if (domValue instanceof GenericAttributeValue) {
final GenericAttributeValue value = (GenericAttributeValue)domValue;
final XmlAttributeValue attributeValue = value.getXmlAttributeValue();
return attributeValue == null ? value.getXmlAttribute() : attributeValue;
} else {
return domValue.getXmlTag();
}
}
public static List<? extends DomElement> getIdentitySiblings(DomElement element) {
final GenericDomValue nameDomElement = element.getGenericInfo().getNameDomElement(element);
if (nameDomElement == null) return Collections.emptyList();
final NameValue nameValue = nameDomElement.getAnnotation(NameValue.class);
if (nameValue == null || !nameValue.unique()) return Collections.emptyList();
final String stringValue = ElementPresentationManager.getElementName(element);
if (stringValue == null) return Collections.emptyList();
final DomElement scope = element.getManager().getIdentityScope(element);
if (scope == null) return Collections.emptyList();
final DomGenericInfo domGenericInfo = scope.getGenericInfo();
final String tagName = element.getXmlElementName();
final DomCollectionChildDescription childDescription =
domGenericInfo.getCollectionChildDescription(tagName, element.getXmlElementNamespaceKey());
if (childDescription != null) {
final ArrayList<DomElement> list = new ArrayList<DomElement>(childDescription.getValues(scope));
list.remove(element);
return list;
}
return Collections.emptyList();
}
public static <T> List<T> getChildrenOfType(@NotNull final DomElement parent, final Class<T> type) {
final List<T> result = new SmartList<T>();
parent.acceptChildren(new DomElementVisitor() {
@Override
public void visitDomElement(final DomElement element) {
if (type.isInstance(element)) {
result.add((T)element);
}
}
});
return result;
}
public static List<DomElement> getDefinedChildren(@NotNull final DomElement parent, final boolean tags, final boolean attributes) {
if (parent instanceof MergedObject) {
final SmartList<DomElement> result = new SmartList<DomElement>();
parent.acceptChildren(new DomElementVisitor() {
@Override
public void visitDomElement(final DomElement element) {
if (hasXml(element)) {
result.add(element);
}
}
});
return result;
}
ProgressManager.checkCanceled();
if (parent instanceof GenericAttributeValue) return Collections.emptyList();
if (parent instanceof DomFileElement) {
final DomFileElement element = (DomFileElement)parent;
return tags ? Arrays.asList(element.getRootElement()) : Collections.<DomElement>emptyList();
}
final XmlElement xmlElement = parent.getXmlElement();
if (xmlElement instanceof XmlTag) {
XmlTag tag = (XmlTag) xmlElement;
final DomManager domManager = parent.getManager();
final SmartList<DomElement> result = new SmartList<DomElement>();
if (attributes) {
for (final XmlAttribute attribute : tag.getAttributes()) {
if (!attribute.isValid()) {
LOG.error("Invalid attr: parent.valid=" + tag.isValid());
continue;
}
GenericAttributeValue element = domManager.getDomElement(attribute);
if (checkHasXml(attribute, element)) {
ContainerUtil.addIfNotNull(element, result);
}
}
}
if (tags) {
for (final XmlTag subTag : tag.getSubTags()) {
if (!subTag.isValid()) {
LOG.error("Invalid subtag: parent.valid=" + tag.isValid());
continue;
}
DomElement element = domManager.getDomElement(subTag);
if (checkHasXml(subTag, element)) {
ContainerUtil.addIfNotNull(element, result);
}
}
}
return result;
}
return Collections.emptyList();
}
private static boolean checkHasXml(XmlElement psi, DomElement dom) {
if (dom != null && !hasXml(dom)) {
LOG.error("No xml for dom " + dom + "; attr=" + psi + ", physical=" + psi.isPhysical());
return false;
}
return true;
}
public static <T> List<T> getDefinedChildrenOfType(@NotNull final DomElement parent, final Class<T> type, boolean tags, boolean attributes) {
return ContainerUtil.findAll(getDefinedChildren(parent, tags, attributes), type);
}
public static <T> List<T> getDefinedChildrenOfType(@NotNull final DomElement parent, final Class<T> type) {
return getDefinedChildrenOfType(parent, type, true, true);
}
@Nullable
public static DomElement findDuplicateNamedValue(DomElement element, String newName) {
return ElementPresentationManager.findByName(getIdentitySiblings(element), newName);
}
public static boolean isAncestor(@NotNull DomElement ancestor, @NotNull DomElement descendant, boolean strict) {
if (!strict && ancestor.equals(descendant)) return true;
final DomElement parent = descendant.getParent();
return parent != null && isAncestor(ancestor, parent, false);
}
public static void acceptAvailableChildren(final DomElement element, final DomElementVisitor visitor) {
final XmlTag tag = element.getXmlTag();
if (tag != null) {
for (XmlTag xmlTag : tag.getSubTags()) {
final DomElement childElement = element.getManager().getDomElement(xmlTag);
if (childElement != null) {
childElement.accept(visitor);
}
}
}
}
public static Collection<Class> getAllInterfaces(final Class aClass, final Collection<Class> result) {
final Class[] interfaces = aClass.getInterfaces();
ContainerUtil.addAll(result, interfaces);
if (aClass.getSuperclass() != null) {
getAllInterfaces(aClass.getSuperclass(), result);
}
for (Class anInterface : interfaces) {
getAllInterfaces(anInterface, result);
}
return result;
}
@Nullable
public static <T> T getParentOfType(final DomElement element, final Class<T> requiredClass, final boolean strict) {
for (DomElement curElement = strict && element != null? element.getParent() : element;
curElement != null;
curElement = curElement.getParent()) {
if (requiredClass.isInstance(curElement)) {
return (T)curElement;
}
}
return null;
}
@Nullable
public static <T> T getContextElement(@Nullable final Editor editor, Class<T> clazz) {
final DomElement element = getContextElement(editor);
return getParentOfType(element, clazz, false);
}
@Nullable
public static DomElement getContextElement(@Nullable final Editor editor) {
if(editor == null) return null;
final Project project = editor.getProject();
if (project == null) return null;
final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
if (!(file instanceof XmlFile)) {
return null;
}
return getDomElement(file.findElementAt(editor.getCaretModel().getOffset()));
}
@Nullable
public static DomElement getDomElement(final Editor editor, final PsiFile file) {
return getDomElement(file.findElementAt(editor.getCaretModel().getOffset()));
}
@Nullable
public static DomElement getDomElement(@Nullable final PsiElement element) {
if (element == null) return null;
final Project project = element.getProject();
final DomManager domManager = DomManager.getDomManager(project);
final XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class, false);
if (attr != null) {
final GenericAttributeValue value = domManager.getDomElement(attr);
if (value != null) return value;
}
XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class, false);
while (tag != null) {
final DomElement domElement = domManager.getDomElement(tag);
if(domElement != null) return domElement;
tag = tag.getParentTag();
}
return null;
}
@NotNull
public static <T extends DomElement> T getOriginalElement(@NotNull final T domElement) {
final XmlElement psiElement = domElement.getXmlElement();
if (psiElement == null) return domElement;
final PsiFile psiFile = psiElement.getContainingFile().getOriginalFile();
final TextRange range = psiElement.getTextRange();
final PsiElement element = psiFile.findElementAt(range.getStartOffset());
final int maxLength = range.getLength();
final boolean isAttribute = psiElement instanceof XmlAttribute;
final Class<? extends XmlElement> clazz = isAttribute ? XmlAttribute.class : XmlTag.class;
final DomManager domManager = domElement.getManager();
DomElement current = null;
for (XmlElement next = PsiTreeUtil.getParentOfType(element, clazz, false);
next != null && next.getTextLength() <= maxLength;
next = PsiTreeUtil.getParentOfType(next, clazz, true)) {
current = isAttribute? domManager.getDomElement((XmlAttribute)next) : domManager.getDomElement((XmlTag)next);
if (current != null && domElement.getClass() != current.getClass()) current = null;
}
return (T)current;
}
public static <T extends DomElement> T addElementAfter(@NotNull final T anchor) {
final DomElement parent = anchor.getParent();
final DomCollectionChildDescription childDescription = (DomCollectionChildDescription)anchor.getChildDescription();
assert parent != null;
final List<? extends DomElement> list = childDescription.getValues(parent);
final int i = list.indexOf(anchor);
assert i >= 0;
return (T)childDescription.addValue(parent, i + 1);
}
@Nullable
public static <T extends DomElement> T findDomElement(@Nullable final PsiElement element, final Class<T> beanClass) {
return findDomElement(element, beanClass, true);
}
@Nullable
public static <T extends DomElement> T findDomElement(@Nullable final PsiElement element, final Class<T> beanClass, boolean strict) {
if (element == null) return null;
XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class, strict);
DomElement domElement;
while (tag != null) {
domElement = DomManager.getDomManager(tag.getProject()).getDomElement(tag);
if (domElement != null) {
return domElement.getParentOfType(beanClass, false);
}
tag = tag.getParentTag();
}
return null;
}
public static <T extends DomElement> DomFileElement<T> getFileElement(@NotNull DomElement element) {
if (element instanceof DomFileElement) {
return (DomFileElement)element;
}
DomFileElement fileElement = element.getUserData(FILE_ELEMENT_KEY);
if (fileElement == null) {
DomElement parent = element.getParent();
if (parent != null) {
fileElement = getFileElement(parent);
}
element.putUserData(FILE_ELEMENT_KEY, fileElement);
}
return fileElement;
}
@NotNull
public static XmlFile getFile(@NotNull DomElement element) {
return DomService.getInstance().getContainingFile(element);
}
/**
* @param domElement DomElement to search root of
* @return the topmost valid DomElement being a parent of the given one. May be and may be not DomFileElement.
* If root tag has changed, file may lose its domness, so there will be no DomFileElement, but the inner DomElement's
* will be still alive because the underlying XML tags are valid
*/
@NotNull
public static DomElement getRoot(@NotNull DomElement domElement) {
while (true) {
final DomElement parent = domElement.getParent();
if (parent == null) {
return domElement;
}
domElement = parent;
}
}
public static boolean hasXml(@NotNull DomElement element) {
return element.exists();
}
public static Pair<TextRange, PsiElement> getProblemRange(final XmlTag tag) {
final PsiElement startToken = XmlTagUtil.getStartTagNameElement(tag);
if (startToken == null) {
return Pair.create(tag.getTextRange(), (PsiElement)tag);
}
return Pair.create(startToken.getTextRange().shiftRight(-tag.getTextRange().getStartOffset()), (PsiElement)tag);
}
@SuppressWarnings("ForLoopReplaceableByForEach")
public static <T extends DomElement> List<T> getChildrenOf(DomElement parent, final Class<T> type) {
final List<T> list = new SmartList<T>();
List<? extends AbstractDomChildrenDescription> descriptions = parent.getGenericInfo().getChildrenDescriptions();
for (int i = 0, descriptionsSize = descriptions.size(); i < descriptionsSize; i++) {
AbstractDomChildrenDescription description = descriptions.get(i);
if (description.getType() instanceof Class && type.isAssignableFrom((Class<?>)description.getType())) {
List<T> values = (List<T>)description.getValues(parent);
for (int j = 0, valuesSize = values.size(); j < valuesSize; j++) {
T value = values.get(j);
if (value.exists()) {
list.add(value);
}
}
}
}
return list;
}
}