blob: 5472ce3a3e20b528c668285298b86401759b1199 [file] [log] [blame]
/*
* Copyright 2007 Sascha Weinreuter
*
* 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.intellij.plugins.relaxNG.model.descriptors;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import com.intellij.xml.XmlElementsGroup;
import com.intellij.xml.XmlNSDescriptor;
import org.intellij.plugins.relaxNG.compact.RncElementTypes;
import org.intellij.plugins.relaxNG.compact.RncFileType;
import org.intellij.plugins.relaxNG.validation.RngSchemaValidator;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
import org.kohsuke.rngom.digested.*;
import org.kohsuke.rngom.nc.NameClass;
import org.kohsuke.rngom.nc.NameClassVisitor;
import org.xml.sax.Locator;
import javax.xml.namespace.QName;
import java.util.*;
public class RngElementDescriptor implements XmlElementDescriptor {
private static final Key<ParameterizedCachedValue<XmlElementDescriptor, RngElementDescriptor>> DESCR_KEY = Key.create("DESCR");
private static final Key<ParameterizedCachedValue<XmlAttributeDescriptor[], RngElementDescriptor>> ATTRS_KEY = Key.create("ATTRS");
protected static final XmlElementDescriptor NULL = null;
private final DElementPattern myElementPattern;
protected final RngNsDescriptor myNsDescriptor;
private volatile SmartPsiElementPointer<? extends PsiElement> myDeclaration;
RngElementDescriptor(RngNsDescriptor nsDescriptor, DElementPattern pattern) {
myNsDescriptor = nsDescriptor;
myElementPattern = pattern;
}
@Override
public String getQualifiedName() {
final QName qName = getQName();
return qName != null ? format(qName, "") : "#unknown";
}
@Override
public String getDefaultName() {
return getName();
}
@Override
public XmlElementDescriptor[] getElementsDescriptors(XmlTag context) {
if (context == null) {
return EMPTY_ARRAY;
}
// TODO: Not sure why this is needed. IDEA sometimes asks us for descriptors with a context that doesn't match our
// element pattern. At least at namespace boundaries...
final DElementPattern pattern;
final XmlElementDescriptor descriptor = myNsDescriptor.getElementDescriptor(context);
if (descriptor instanceof RngElementDescriptor) {
final DElementPattern p = ((RngElementDescriptor)descriptor).myElementPattern;
pattern = p != null ? p : myElementPattern;
} else {
pattern = myElementPattern;
}
final List<DElementPattern> patterns = ChildElementFinder.find(2, pattern);
return myNsDescriptor.convertElementDescriptors(patterns);
}
protected XmlElementDescriptor findElementDescriptor(XmlTag childTag) {
final List<DElementPattern> patterns = ChildElementFinder.find(2, myElementPattern);
final XmlElementDescriptor d = myNsDescriptor.findDescriptor(childTag, patterns);
return d == null ? NULL : d;
}
public final XmlElementDescriptor getElementDescriptor(final XmlTag childTag) {
return getElementDescriptor(childTag, null);
}
@Override
public final XmlElementDescriptor getElementDescriptor(final XmlTag childTag, XmlTag contextTag) {
final XmlElementDescriptor value = getCachedValue(childTag, this, DESCR_KEY, new ParameterizedCachedValueProvider<XmlElementDescriptor, RngElementDescriptor>() {
@Override
public CachedValueProvider.Result<XmlElementDescriptor> compute(RngElementDescriptor p) {
final XmlElementDescriptor descriptor = p.findElementDescriptor(childTag);
return CachedValueProvider.Result.create(descriptor, p.getDependences(), childTag);
}
});
return value == NULL ? null : value;
}
@Override
public final XmlAttributeDescriptor[] getAttributesDescriptors(@Nullable final XmlTag context) {
if (context != null) {
return getCachedValue(context, this, ATTRS_KEY, new ParameterizedCachedValueProvider<XmlAttributeDescriptor[], RngElementDescriptor>() {
@Override
public CachedValueProvider.Result<XmlAttributeDescriptor[]> compute(RngElementDescriptor p) {
final XmlAttributeDescriptor[] value = p.collectAttributeDescriptors(context);
return CachedValueProvider.Result.create(value, p.getDependences(), context);
}
});
} else {
return collectAttributeDescriptors(null);
}
}
private static <D extends PsiElement, T, P> T getCachedValue(D context, P p, Key<ParameterizedCachedValue<T, P>> key, ParameterizedCachedValueProvider<T, P> provider) {
final CachedValuesManager mgr = CachedValuesManager.getManager(context.getProject());
return mgr.getParameterizedCachedValue(context, key, provider, false, p);
}
protected XmlAttributeDescriptor[] collectAttributeDescriptors(@Nullable XmlTag context) {
return computeAttributeDescriptors(AttributeFinder.find((QName)null, myElementPattern));
}
protected XmlAttributeDescriptor[] computeAttributeDescriptors(final Map<DAttributePattern, Pair<? extends Map<String, String>, Boolean>> map) {
final Map<QName, RngXmlAttributeDescriptor> name2descriptor = new HashMap<QName, RngXmlAttributeDescriptor>();
for (DAttributePattern pattern : map.keySet()) {
final Pair<? extends Map<String, String>, Boolean> value = map.get(pattern);
for (QName name : pattern.getName().listNames()) {
RngXmlAttributeDescriptor descriptor = name2descriptor.get(name);
final RngXmlAttributeDescriptor newDescriptor = new RngXmlAttributeDescriptor(this, pattern, value.first, value.second);
if (descriptor == null) {
descriptor = newDescriptor;
}
else {
descriptor = descriptor.mergeWith(newDescriptor);
}
name2descriptor.put(name, descriptor);
}
}
final Collection<RngXmlAttributeDescriptor> result = name2descriptor.values();
return result.toArray(new RngXmlAttributeDescriptor[result.size()]);
}
@Override
public final XmlAttributeDescriptor getAttributeDescriptor(String attributeName, @Nullable XmlTag context) {
return getAttributeDescriptor("", attributeName);
}
@Override
public final XmlAttributeDescriptor getAttributeDescriptor(XmlAttribute attribute) {
return getAttributeDescriptor(attribute.getNamespace(), attribute.getLocalName());
}
protected XmlAttributeDescriptor getAttributeDescriptor(String namespace, String localName) {
final QName qname = new QName(namespace, localName);
return computeAttributeDescriptor(AttributeFinder.find(qname, myElementPattern));
}
protected XmlAttributeDescriptor computeAttributeDescriptor(final Map<DAttributePattern, Pair<? extends Map<String, String>, Boolean>> attributes) {
if (attributes.size() > 0) {
RngXmlAttributeDescriptor d = null;
final Set<DAttributePattern> patterns = attributes.keySet();
for (DAttributePattern pattern : patterns) {
final Pair<? extends Map<String, String>, Boolean> pair = attributes.get(pattern);
final RngXmlAttributeDescriptor a =
new RngXmlAttributeDescriptor(this, pattern, pair.first, pair.second);
if (d == null) {
d = a;
} else {
d = d.mergeWith(a);
}
}
return d;
} else {
return null;
}
}
@Override
public XmlNSDescriptor getNSDescriptor() {
return myNsDescriptor;
}
@Override
public XmlElementsGroup getTopGroup() {
return null;
}
// is this actually used anywhere?
@Override
public int getContentType() {
final DPattern child = myElementPattern.getChild();
if (child instanceof DEmptyPattern) {
return CONTENT_TYPE_EMPTY;
} else if (child instanceof DTextPattern) {
return CONTENT_TYPE_MIXED;
} else if (child instanceof DElementPattern) {
return ((DElementPattern)child).getName().accept(MyNameClassVisitor.INSTANCE);
} else {
return CONTENT_TYPE_CHILDREN;
}
}
@Override
public String getDefaultValue() {
return null;
}
@Override
public PsiElement getDeclaration() {
final SmartPsiElementPointer<? extends PsiElement> declaration = myDeclaration;
if (declaration != null) {
final PsiElement element = declaration.getElement();
if (element != null && element.isValid()) {
return element;
}
}
final PsiElement decl = myNsDescriptor.getDeclaration();
if (decl == null/* || !decl.isValid()*/) {
myDeclaration = null;
return null;
}
final PsiElement element = getDeclarationImpl(decl, myElementPattern.getLocation());
if (element != null && element != decl) {
myDeclaration = SmartPointerManager.getInstance(decl.getProject()).createSmartPsiElementPointer(element);
}
return element;
}
public PsiElement getDeclaration(Locator location) {
final PsiElement element = myNsDescriptor.getDeclaration();
if (element == null) {
return null;
}
return getDeclarationImpl(element, location);
}
private static PsiElement getDeclarationImpl(PsiElement decl, Locator location) {
final VirtualFile virtualFile = RngSchemaValidator.findVirtualFile(location.getSystemId());
if (virtualFile == null) {
return decl;
}
final Project project = decl.getProject();
final PsiFile file = PsiManager.getInstance(project).findFile(virtualFile);
if (file == null) {
return decl;
}
final int column = location.getColumnNumber();
final int line = location.getLineNumber();
final Document document = PsiDocumentManager.getInstance(project).getDocument(file);
assert document != null;
if (line <= 0 || document.getLineCount() < line - 1) {
return decl;
}
final int startOffset = document.getLineStartOffset(line - 1);
final PsiElement at;
if (column > 0) {
if (decl.getContainingFile().getFileType() == RncFileType.getInstance()) {
final PsiElement rncElement = file.findElementAt(startOffset + column);
final ASTNode pattern = rncElement != null ? TreeUtil.findParent(rncElement.getNode(), RncElementTypes.PATTERN) : null;
final ASTNode nameClass = pattern != null ? pattern.findChildByType(RncElementTypes.NAME_CLASS) : null;
return nameClass != null ? nameClass.getPsi() : rncElement;
}
at = file.findElementAt(startOffset + column - 2);
} else {
PsiElement element = file.findElementAt(startOffset);
at = element != null ? PsiTreeUtil.nextLeaf(element) : null;
}
return PsiTreeUtil.getParentOfType(at, XmlTag.class);
}
@Override
@NonNls
public String getName(PsiElement context) {
final QName qName = getQName();
if (qName == null) {
return "#unknown";
}
final XmlTag xmlTag = PsiTreeUtil.getParentOfType(context, XmlTag.class, false);
final String prefix = xmlTag != null ? xmlTag.getPrefixByNamespace(qName.getNamespaceURI()) : null;
return format(qName, prefix != null ? prefix : qName.getPrefix());
}
@Override
@NonNls
public String getName() {
final QName qName = getQName();
if (qName == null) {
return "#unknown";
}
return qName.getLocalPart();
}
private static String format(QName qName, String p) {
final String localPart = qName.getLocalPart();
return p.length() > 0 ? p + ":" + localPart : localPart;
}
@Nullable
private QName getQName() {
final Iterator<QName> iterator = myElementPattern.getName().listNames().iterator();
if (!iterator.hasNext()) {
return null;
}
return iterator.next();
}
@Override
public void init(PsiElement element) {
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final RngElementDescriptor that = (RngElementDescriptor)o;
if (!myElementPattern.equals(that.myElementPattern)) return false;
return true;
}
public int hashCode() {
return myElementPattern.hashCode();
}
@Override
public Object[] getDependences() {
if (myDeclaration != null) {
return ArrayUtil.append(myNsDescriptor.getDependences(), myDeclaration.getElement());
} else {
return myNsDescriptor.getDependences();
}
}
private static class MyNameClassVisitor implements NameClassVisitor<Integer> {
public static final MyNameClassVisitor INSTANCE = new MyNameClassVisitor();
@Override
public Integer visitAnyName() {
return CONTENT_TYPE_ANY;
}
@Override
public Integer visitAnyNameExcept(NameClass nc) {
return CONTENT_TYPE_ANY;
}
@Override
public Integer visitChoice(NameClass nc1, NameClass nc2) {
return CONTENT_TYPE_CHILDREN;
}
@Override
public Integer visitName(QName name) {
return CONTENT_TYPE_CHILDREN;
}
@Override
public Integer visitNsName(String ns) {
return CONTENT_TYPE_CHILDREN;
}
@Override
public Integer visitNsNameExcept(String ns, NameClass nc) {
return CONTENT_TYPE_CHILDREN;
}
@Override
public Integer visitNull() {
return CONTENT_TYPE_EMPTY;
}
}
public DElementPattern getElementPattern() {
return myElementPattern;
}
}