blob: 58241bdf66dd02ff092c68c22c876d399a20e2f1 [file] [log] [blame]
/*
* Copyright 2000-2011 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 org.intellij.lang.xpath.xslt.context;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.SimpleFieldCache;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import com.intellij.xml.XmlNSDescriptor;
import com.intellij.xml.impl.schema.XmlElementDescriptorImpl;
import com.intellij.xml.impl.schema.XmlNSDescriptorImpl;
import com.intellij.xml.util.XmlUtil;
import gnu.trove.THashSet;
import org.intellij.lang.xpath.XPathFile;
import org.intellij.lang.xpath.context.ContextProvider;
import org.intellij.lang.xpath.context.NamespaceContext;
import org.intellij.lang.xpath.context.VariableContext;
import org.intellij.lang.xpath.psi.XPathExpression;
import org.intellij.lang.xpath.psi.XPathType;
import org.intellij.lang.xpath.validation.inspections.quickfix.XPathQuickFixFactory;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.intellij.lang.xpath.xslt.associations.FileAssociationsManager;
import org.intellij.lang.xpath.xslt.psi.XsltElement;
import org.intellij.lang.xpath.xslt.psi.XsltElementFactory;
import org.intellij.lang.xpath.xslt.psi.XsltVariable;
import org.intellij.lang.xpath.xslt.psi.XsltWithParam;
import org.intellij.lang.xpath.xslt.util.NSDeclTracker;
import org.intellij.lang.xpath.xslt.util.QNameUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.xml.namespace.QName;
import java.util.*;
/*
* Created by IntelliJ IDEA.
* User: sweinreuter
* Date: 08.01.11
*/
public abstract class XsltContextProviderBase extends ContextProvider {
protected static final Set<String> IGNORED_URIS = new THashSet<String>();
static {
IGNORED_URIS.add(XsltSupport.XSLT_NS);
IGNORED_URIS.addAll(XmlUtil.ourSchemaUrisList);
}
private static final SimpleFieldCache<CachedValue<ElementNames>, XsltContextProviderBase> myNamesCache = new SimpleFieldCache<CachedValue<ElementNames>, XsltContextProviderBase>() {
protected CachedValue<ElementNames> compute(final XsltContextProviderBase xsltContextProvider) {
return xsltContextProvider.createCachedValue(xsltContextProvider.getFile());
}
protected CachedValue<ElementNames> getValue(final XsltContextProviderBase xsltContextProvider) {
return xsltContextProvider.myNames;
}
protected void putValue(final CachedValue<ElementNames> elementNamesCachedValue, final XsltContextProviderBase xsltContextProvider) {
xsltContextProvider.myNames = elementNamesCachedValue;
}
};
private CachedValue<ElementNames> myNames;
protected final SmartPsiElementPointer<XmlElement> myContextElement;
protected final FileAssociationsManager myFileAssociationsManager;
protected XsltContextProviderBase(XmlElement element) {
final Project project = element.getProject();
myFileAssociationsManager = FileAssociationsManager.getInstance(project);
myContextElement = SmartPointerManager.getInstance(project).createSmartPsiElementPointer(element);
attachTo(element);
}
@Override
protected boolean isValid() {
return super.isValid() && matchContextType();
}
private boolean matchContextType() {
final PsiFile file = myContextElement.getContainingFile();
return file != null && XsltSupport.getXsltLanguageLevel(file).getXPathVersion() == getContextType().getVersion();
}
private static void fillFromSchema(PsiFile file, ElementNames names) {
if (!(file instanceof XmlFile)) return;
final XmlFile f = (XmlFile)file;
final XmlDocument d = f.getDocument();
if (d == null) return;
final XmlTag rootTag = d.getRootTag();
if (rootTag == null) return;
//noinspection unchecked
names.dependencies.add(new NSDeclTracker(rootTag));
try {
final Map<String, String> namespaceDeclarations = rootTag.getLocalNamespaceDeclarations();
final Collection<String> prefixes = namespaceDeclarations.keySet();
final XmlElementFactory ef = XmlElementFactory.getInstance(file.getProject());
int noSchemaNamespaces = 0;
for (String prefix : prefixes) {
final String namespace = namespaceDeclarations.get(prefix);
if (isIgnoredNamespace(prefix, namespace)) continue;
final XmlTag tag = ef.createTagFromText("<dummy-tag xmlns='" + namespace + "' />", XMLLanguage.INSTANCE);
final XmlDocument document = PsiTreeUtil.getParentOfType(tag, XmlDocument.class);
final XmlNSDescriptor rootDescriptor = tag.getNSDescriptor(tag.getNamespace(), true);
if (rootDescriptor == null ||
(rootDescriptor instanceof XmlNSDescriptorImpl && ((XmlNSDescriptorImpl)rootDescriptor).getTag() == null) ||
!rootDescriptor.getDeclaration().isPhysical()) {
final QName any = QNameUtil.createAnyLocalName(namespace);
names.elementNames.add(any);
names.attributeNames.add(any);
noSchemaNamespaces++;
continue;
}
//noinspection unchecked
names.dependencies.add(rootDescriptor.getDescriptorFile());
//noinspection unchecked
final Set<XmlElementDescriptor> history = new THashSet<XmlElementDescriptor>(150);
final XmlElementDescriptor[] e = rootDescriptor.getRootElementsDescriptors(document);
try {
for (XmlElementDescriptor descriptor : e) {
processElementDescriptors(descriptor, tag, names, history, 0);
}
} catch (StopProcessingException e1) {
Logger.getInstance(XsltContextProviderBase.class).error("Maximum recursion depth reached. Missing equals()/hashCode() implementation?", StringUtil
.join(history, new Function<XmlElementDescriptor, String>() {
@Override
public String fun(XmlElementDescriptor descriptor) {
return descriptor.getClass().getName() + "[" + descriptor.getQualifiedName() + "]";
}
}, ", "));
}
}
names.validateNames = names.elementNames.size() > noSchemaNamespaces;
// final QName any = QNameUtil.createAnyLocalName("");
// names.elementNames.add(any);
// names.attributeNames.add(any);
} catch (IncorrectOperationException e) {
Logger.getInstance(XsltContextProvider.class.getName()).error(e);
}
}
private static boolean isIgnoredNamespace(String prefix, String namespace) {
return IGNORED_URIS.contains(namespace) || prefix.length() == 0 || "xmlns".equals(prefix);
}
private static class StopProcessingException extends Exception {
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
private static void processElementDescriptors(XmlElementDescriptor descriptor, XmlTag tag, ElementNames names, Set<XmlElementDescriptor> history, int depth)
throws StopProcessingException {
if (!history.add(descriptor) || ++depth == 200) {
if (depth == 200) {
throw new StopProcessingException();
}
return;
}
final String namespace = descriptor instanceof XmlElementDescriptorImpl
? ((XmlElementDescriptorImpl)descriptor).getNamespace()
: tag.getNamespace();
names.elementNames.add(new QName(namespace, descriptor.getName()));
final XmlAttributeDescriptor[] attributesDescriptors = descriptor.getAttributesDescriptors(null);
for (XmlAttributeDescriptor attributesDescriptor : attributesDescriptors) {
final String localPart = attributesDescriptor.getName();
if (!"xmlns".equals(localPart)) names.attributeNames.add(new QName(localPart));
}
final XmlElementDescriptor[] descriptors = descriptor.getElementsDescriptors(tag);
for (XmlElementDescriptor elem : descriptors) {
processElementDescriptors(elem, tag, names, history, depth);
}
}
public PsiFile[] getRelatedFiles(final XPathFile file) {
final XmlAttribute attribute = PsiTreeUtil.getContextOfType(file, XmlAttribute.class, false);
assert attribute != null;
final PsiFile psiFile = attribute.getContainingFile();
assert psiFile != null;
final List<PsiFile> files = new ArrayList<PsiFile>();
psiFile.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlAttribute(XmlAttribute attribute) {
final PsiFile[] _files = XsltSupport.getFiles(attribute);
for (PsiFile _file : _files) {
if (_file != file) files.add(_file);
}
}
});
return PsiUtilCore.toPsiFileArray(files);
}
@Nullable
public XmlElement getContextElement() {
return myContextElement.getElement();
}
@NotNull
public XPathType getExpectedType(XPathExpression expr) {
final XmlTag tag = PsiTreeUtil.getContextOfType(expr, XmlTag.class, true);
if (tag != null && XsltSupport.isXsltTag(tag)) {
final XsltElement element = XsltElementFactory.getInstance().wrapElement(tag, XsltElement.class);
if (element instanceof XsltVariable) {
return ((XsltVariable)element).getType();
} else {
final XmlAttribute attr = PsiTreeUtil.getContextOfType(expr, XmlAttribute.class, true);
if (attr != null) {
if (element instanceof XsltWithParam) {
final XmlAttribute nameAttr = tag.getAttribute("name", null);
if (nameAttr != null) {
final XmlAttributeValue valueElement = nameAttr.getValueElement();
if (valueElement != null) {
final PsiReference[] references = valueElement.getReferences();
for (PsiReference reference : references) {
final PsiElement psiElement = reference.resolve();
if (psiElement instanceof XsltVariable) {
return ((XsltVariable)psiElement).getType();
}
}
}
}
} else {
final String name = attr.getName();
return getTypeForTag(tag, name);
}
}
}
}
return XPathType.UNKNOWN;
}
protected XPathType getTypeForTag(XmlTag tag, String attribute) {
String tagName = tag.getLocalName();
if ("select".equals(attribute)) {
if ("copy-of".equals(tagName) || "for-each".equals(tagName) || "apply-templates".equals(tagName)) {
return XPathType.NODESET;
} else if ("value-of".equals(tagName) || "sort".equals(tagName)) {
return XPathType.STRING;
}
return XPathType.ANY;
} else if ("test".equals(attribute)) {
if ("if".equals(tagName) || "when".equals(tagName)) {
return XPathType.BOOLEAN;
}
} else if ("number".equals(attribute)) {
if ("value".equals(tagName)) {
return XPathType.NUMBER;
}
}
return XPathType.UNKNOWN;
}
@NotNull
public NamespaceContext getNamespaceContext() {
return XsltNamespaceContext.NAMESPACE_CONTEXT;
}
@NotNull
public VariableContext getVariableContext() {
return XsltVariableContext.INSTANCE;
}
@Nullable
public Set<QName> getAttributes(boolean forValidation) {
final ElementNames names = getNames(getFile());
if (names != null) {
return !forValidation || names.validateNames ? names.attributeNames : null;
}
return null;
}
@Nullable
public Set<QName> getElements(boolean forValidation) {
final ElementNames names = getNames(getFile());
if (names != null) {
return !forValidation || names.validateNames ? names.elementNames : null;
}
return null;
}
@Nullable
private ElementNames getNames(@Nullable PsiFile file) {
if (file == null) return null;
return myNamesCache.get(this).getValue();
}
private CachedValue<ElementNames> createCachedValue(final PsiFile file) {
return CachedValuesManager.getManager(file.getProject()).createCachedValue(new CachedValueProvider<ElementNames>() {
public Result<ElementNames> compute() {
final ElementNames names = new ElementNames();
final PsiFile[] associations = myFileAssociationsManager.getAssociationsFor(file, FileAssociationsManager.XML_FILES);
if (associations.length == 0) {
fillFromSchema(file, names);
} else {
names.validateNames = true;
//noinspection unchecked
ContainerUtil.addAll(names.dependencies, associations);
}
//noinspection unchecked
names.dependencies.add(myFileAssociationsManager);
for (PsiFile file : associations) {
if (!(file instanceof XmlFile)) continue;
file.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
names.elementNames.add(QNameUtil.createQName(tag));
super.visitXmlTag(tag);
}
@Override
public void visitXmlAttribute(XmlAttribute attribute) {
if (!attribute.isNamespaceDeclaration()) {
names.attributeNames.add(QNameUtil.createQName(attribute));
}
super.visitXmlAttribute(attribute);
}
});
}
//noinspection unchecked
return new Result<ElementNames>(names, ArrayUtil.toObjectArray(names.dependencies));
}
}, false);
}
@Nullable
private PsiFile getFile() {
final XmlElement element = getContextElement();
if (element == null) {
return null;
}
return element.getContainingFile().getOriginalFile();
}
@NotNull
public XPathQuickFixFactory getQuickFixFactory() {
return XsltQuickFixFactory.INSTANCE;
}
static class ElementNames {
boolean validateNames;
final Set<QName> elementNames = new HashSet<QName>();
final Set<QName> attributeNames = new HashSet<QName>();
@SuppressWarnings({"RawUseOfParameterizedType"})
final Set dependencies = new HashSet();
}
}