blob: 8f0426889c3fa7cb42931689229bc632ec1e0c5d [file] [log] [blame]
/*
* Copyright 2005 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.lang.xpath.context;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.lang.Language;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlFile;
import org.intellij.lang.xpath.XPathFile;
import org.intellij.lang.xpath.XPathFileType;
import org.intellij.lang.xpath.context.functions.DefaultFunctionContext;
import org.intellij.lang.xpath.context.functions.FunctionContext;
import org.intellij.lang.xpath.psi.*;
import org.intellij.lang.xpath.validation.inspections.quickfix.XPathQuickFixFactory;
import org.intellij.lang.xpath.xslt.context.XsltNamespaceContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.xml.namespace.QName;
import java.util.Set;
public abstract class ContextProvider {
/**
* ContextProvider instance, must be available (aka @NotNull) at every XPathFile instance by getCopyableUserData().
* It gets lost with putUserData()!
*/
private static final Key<ContextProvider> KEY = Key.create("CONTEXT_PROVIDER");
private static final Key<Boolean> XML_FILE_WITH_XPATH_INJECTTION = Key.create("XML_FILE_WITH_XPATH_INJECTTION");
private volatile FunctionContext myFunctionContext;
protected ContextProvider() {
}
@NotNull
public abstract ContextType getContextType();
@Nullable
public abstract XmlElement getContextElement();
@Nullable
public abstract NamespaceContext getNamespaceContext();
@Nullable
public abstract VariableContext getVariableContext();
@NotNull
public FunctionContext getFunctionContext() {
FunctionContext context = myFunctionContext;
if (context == null) {
context = createFunctionContext();
}
return (myFunctionContext = context);
}
protected FunctionContext createFunctionContext() {
return DefaultFunctionContext.getInstance(getContextType());
}
@NotNull
public XPathQuickFixFactory getQuickFixFactory() {
return XPathQuickFixFactoryImpl.INSTANCE;
}
@Nullable
public abstract Set<QName> getAttributes(boolean forValidation);
@Nullable
public abstract Set<QName> getElements(boolean forValidation);
public void attachTo(PsiFile file) {
assert file instanceof XPathFile;
file.putCopyableUserData(KEY, this);
}
protected final void attachTo(XmlElement context) {
context.putCopyableUserData(KEY, this);
}
@SuppressWarnings({ "ClassReferencesSubclass" })
public static void copy(@NotNull PsiFile file1, @NotNull XPathFile file2) {
final ContextProvider contextProvider = getContextProvider(file1);
if (!(contextProvider instanceof DefaultProvider)) {
contextProvider.attachTo(file2);
}
}
@NotNull
public static ContextProvider getContextProvider(PsiFile psiFile) {
ContextProvider provider = psiFile.getCopyableUserData(KEY);
if (provider != null && provider.isValid()) {
return provider;
}
final PsiElement context = psiFile.getContext();
if (context != null) {
provider = context.getCopyableUserData(KEY);
if (provider != null && provider.isValid()) {
return provider;
}
}
return getFromExtensionOrDefault(psiFile);
}
protected boolean isValid() {
final XmlElement contextElement = getContextElement();
return contextElement != null && contextElement.isValid();
}
@SuppressWarnings({ "ClassReferencesSubclass" })
private static ContextProvider getFromExtensionOrDefault(PsiFile psiFile) {
if (psiFile instanceof XPathFile) {
final ContextProvider instance = ContextProviderExtension.getInstance((XPathFile)psiFile);
if (instance != null) {
instance.attachTo(psiFile);
return instance;
}
}
return new DefaultProvider(PsiTreeUtil.getContextOfType(psiFile, XmlElement.class, true), psiFile.getLanguage());
}
@SuppressWarnings({ "ClassReferencesSubclass" })
@NotNull
public static ContextProvider getContextProvider(PsiElement element) {
return element instanceof XPathElement ?
getContextProvider(element instanceof XPathFile ?
(PsiFile)element :
element.getContainingFile()) :
new DefaultProvider(PsiTreeUtil.getParentOfType(element, XmlElement.class, false));
}
public PsiFile[] getRelatedFiles(XPathFile file) {
return PsiFile.EMPTY_ARRAY;
}
@NotNull
public XPathType getExpectedType(XPathExpression expr) {
return XPathType.UNKNOWN;
}
@Nullable
public QName getQName(QNameElement element) {
final PrefixedName qname = element.getQName();
return qname != null ? getQName(qname, element) : null;
}
@Nullable
public QName getQName(@NotNull PrefixedName qName, XPathElement context) {
final String prefix = qName.getPrefix();
final NamespaceContext namespaceContext = getNamespaceContext();
if (namespaceContext != null) {
if (prefix != null) {
final XmlElement element = PsiTreeUtil.getContextOfType(context, XmlElement.class, true);
final String namespaceURI = namespaceContext.getNamespaceURI(prefix, element);
return namespaceURI != null && namespaceURI.length() > 0 ? new QName(namespaceURI, qName.getLocalName(), prefix) : null;
} else if (context.getXPathVersion() == XPathVersion.V2){
if (isDefaultCapableElement(context)) {
final String namespace = namespaceContext.getDefaultNamespace(getContextElement());
if (namespace != null) {
return new QName(namespace, qName.getLocalName());
}
}
}
return new QName(null, qName.getLocalName(), "");
} else if (qName.getPrefix() == null) {
return QName.valueOf(qName.getLocalName());
} else {
return null;
}
}
private static boolean isDefaultCapableElement(XPathElement context) {
// http://www.w3.org/TR/xslt20/#unprefixed-qnames
return (context instanceof XPathNodeTest && ((XPathNodeTest)context).getPrincipalType() == XPathNodeTest.PrincipalType.ELEMENT)
|| context instanceof XPath2TypeElement;
}
public static boolean hasXPathInjections(XmlFile file) {
return Boolean.TRUE.equals(file.getUserData(XML_FILE_WITH_XPATH_INJECTTION));
}
public static final class DefaultProvider extends ContextProvider {
public static NamespaceContext NULL_NAMESPACE_CONTEXT = null;
private final XmlElement myContextElement;
private final ContextType myContextType;
private final NamespaceContext myNamespaceContext;
DefaultProvider(XmlElement contextElement) {
myContextElement = contextElement;
myContextType = ContextType.PLAIN;
if (myContextElement != null) {
myNamespaceContext = XsltNamespaceContext.NAMESPACE_CONTEXT;
setXPathInjected(myContextElement.getContainingFile());
} else {
myNamespaceContext = NULL_NAMESPACE_CONTEXT;
}
}
public DefaultProvider(XmlElement element, Language language) {
myContextElement = element;
myContextType = language == XPathFileType.XPATH2.getLanguage() ? ContextType.PLAIN_V2 : ContextType.PLAIN;
if (myContextElement != null) {
myNamespaceContext = XsltNamespaceContext.NAMESPACE_CONTEXT;
setXPathInjected(myContextElement.getContainingFile());
} else {
myNamespaceContext = NULL_NAMESPACE_CONTEXT;
}
}
private static void setXPathInjected(final PsiFile file) {
final Boolean flag = file.getUserData(XML_FILE_WITH_XPATH_INJECTTION);
// This is a very ugly hack, but it is required to make the implicit usages provider recognize namespace declarations used from
// within injected XPath fragments during IDEA startup. Otherwise, the namespace declarations may be marked as unused until the
// first edit in the file.
// Another (possibly perferred) solution might be to make org.intellij.lang.xpath.xslt.impl.XsltImplicitUsagesProvider run
// unconditionally or - even better - pull its functionality into the platform.
if (flag == null) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if (file.getUserData(XML_FILE_WITH_XPATH_INJECTTION) == null) {
file.putUserData(XML_FILE_WITH_XPATH_INJECTTION, Boolean.TRUE);
if (!ApplicationManager.getApplication().isUnitTestMode()) { // TODO workaround for highlighting tests
DaemonCodeAnalyzer.getInstance(file.getProject()).restart(file);
}
}
}
});
}
}
@NotNull
public ContextType getContextType() {
return myContextType;
}
@Nullable
public XmlElement getContextElement() {
return myContextElement;
}
@Nullable
public NamespaceContext getNamespaceContext() {
return myNamespaceContext;
}
@Nullable
public VariableContext getVariableContext() {
return null;
}
@Nullable
public Set<QName> getAttributes(boolean forValidation) {
return null;
}
@Nullable
public Set<QName> getElements(boolean forValidation) {
return null;
}
}
}