blob: dd70733533f810ccd98083ceabd8d6d72fa1b328 [file] [log] [blame]
/*
* Copyright 2000-2013 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.psi.impl.source.xml;
import com.intellij.javaee.ExternalResourceManager;
import com.intellij.javaee.ExternalResourceManagerEx;
import com.intellij.javaee.ImplicitNamespaceDescriptorProvider;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.PomManager;
import com.intellij.pom.PomModel;
import com.intellij.pom.event.PomModelEvent;
import com.intellij.pom.impl.PomTransactionBase;
import com.intellij.pom.xml.XmlAspect;
import com.intellij.pom.xml.impl.events.XmlAttributeSetImpl;
import com.intellij.pom.xml.impl.events.XmlTagNameChangedImpl;
import com.intellij.psi.*;
import com.intellij.psi.impl.meta.MetaRegistry;
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
import com.intellij.psi.impl.source.tree.*;
import com.intellij.psi.impl.source.tree.Factory;
import com.intellij.psi.meta.PsiMetaData;
import com.intellij.psi.meta.PsiMetaOwner;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.tree.ChildRoleBase;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.CharTable;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.PlatformIcons;
import com.intellij.util.containers.BidirectionalMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import com.intellij.xml.XmlExtension;
import com.intellij.xml.XmlNSDescriptor;
import com.intellij.xml.impl.schema.AnyXmlElementDescriptor;
import com.intellij.xml.impl.schema.XmlNSDescriptorImpl;
import com.intellij.xml.index.XmlNamespaceIndex;
import com.intellij.xml.util.XmlTagUtil;
import com.intellij.xml.util.XmlUtil;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
/**
* @author Mike
*/
public class XmlTagImpl extends XmlElementImpl implements XmlTag {
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.xml.XmlTagImpl");
@NonNls private static final String XML_NS_PREFIX = "xml";
private static final RecursionGuard ourGuard = RecursionManager.createGuard("xmlTag");
private static final Key<ParameterizedCachedValue<XmlTag[], XmlTagImpl>> SUBTAGS_KEY = Key.create("subtags");
private static final ParameterizedCachedValueProvider<XmlTag[],XmlTagImpl> CACHED_VALUE_PROVIDER =
new ParameterizedCachedValueProvider<XmlTag[], XmlTagImpl>() {
@Override
public CachedValueProvider.Result<XmlTag[]> compute(XmlTagImpl tag) {
final List<XmlTag> result = new ArrayList<XmlTag>();
tag.fillSubTags(result);
final int s = result.size();
XmlTag[] tags = s > 0 ? ContainerUtil.toArray(result, new XmlTag[s]) : EMPTY;
return CachedValueProvider.Result
.create(tags, PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT, tag.getContainingFile());
}
};
private final int myHC = ourHC++;
private volatile String myName = null;
private volatile String myLocalName;
private volatile XmlAttribute[] myAttributes = null;
private volatile Map<String, String> myAttributeValueMap = null;
private volatile XmlTagValue myValue = null;
private volatile Map<String, CachedValue<XmlNSDescriptor>> myNSDescriptorsMap = null;
private volatile String myCachedNamespace;
private volatile long myModCount;
private volatile XmlElementDescriptor myCachedDescriptor;
private volatile long myDescriptorModCount = -1;
private volatile long myExtResourcesModCount = -1;
private volatile boolean myHasNamespaceDeclarations = false;
private volatile BidirectionalMap<String, String> myNamespaceMap = null;
public XmlTagImpl() {
this(XmlElementType.XML_TAG);
}
protected XmlTagImpl(IElementType type) {
super(type);
}
@Nullable
private static XmlNSDescriptor getDtdDescriptor(@NotNull XmlFile containingFile) {
final XmlDocument document = containingFile.getDocument();
if (document == null) {
return null;
}
final String url = XmlUtil.getDtdUri(document);
if (url == null) {
return null;
}
return document.getDefaultNSDescriptor(url, true);
}
@Nullable
private static String getNSVersion(String ns, final XmlTagImpl xmlTag) {
String versionValue = xmlTag.getAttributeValue("version");
if (versionValue != null && xmlTag.getNamespace().equals(ns)) {
return versionValue;
}
return null;
}
@Override
public final int hashCode() {
return myHC;
}
@Override
public void clearCaches() {
myName = null;
myLocalName = null;
myNamespaceMap = null;
myCachedNamespace = null;
myCachedDescriptor = null;
myDescriptorModCount = -1;
myAttributes = null;
myAttributeValueMap = null;
myHasNamespaceDeclarations = false;
myValue = null;
myNSDescriptorsMap = null;
super.clearCaches();
}
@Override
@NotNull
public PsiReference[] getReferences() {
ProgressManager.checkCanceled();
final ASTNode startTagName = XmlChildRole.START_TAG_NAME_FINDER.findChild(this);
if (startTagName == null) return PsiReference.EMPTY_ARRAY;
final ASTNode endTagName = XmlChildRole.CLOSING_TAG_NAME_FINDER.findChild(this);
List<PsiReference> refs = new ArrayList<PsiReference>();
String prefix = getNamespacePrefix();
TagNameReference startTagRef = TagNameReference.createTagNameReference(this, startTagName, true);
if (startTagRef != null) {
refs.add(startTagRef);
}
if (!prefix.isEmpty()) {
refs.add(createPrefixReference(startTagName, prefix, startTagRef));
}
if (endTagName != null) {
TagNameReference endTagRef = TagNameReference.createTagNameReference(this, endTagName, false);
if (endTagRef != null) {
refs.add(endTagRef);
}
prefix = XmlUtil.findPrefixByQualifiedName(endTagName.getText());
if (StringUtil.isNotEmpty(prefix)) {
refs.add(createPrefixReference(endTagName, prefix, endTagRef));
}
}
// ArrayList.addAll() makes a clone of the collection
//noinspection ManualArrayToCollectionCopy
for (PsiReference ref : ReferenceProvidersRegistry.getReferencesFromProviders(this, XmlTag.class)) {
refs.add(ref);
}
return ContainerUtil.toArray(refs, new PsiReference[refs.size()]);
}
private SchemaPrefixReference createPrefixReference(ASTNode startTagName, String prefix, TagNameReference tagRef) {
return new SchemaPrefixReference(this, TextRange.from(startTagName.getStartOffset() - getStartOffset(), prefix.length()), prefix,
tagRef);
}
@Override
public XmlNSDescriptor getNSDescriptor(final String namespace, boolean strict) {
final XmlTag parentTag = getParentTag();
if (parentTag == null && namespace.equals(XmlUtil.XHTML_URI)) {
final XmlNSDescriptor descriptor = getDtdDescriptor(XmlUtil.getContainingFile(this));
if (descriptor != null) {
return descriptor;
}
}
Map<String, CachedValue<XmlNSDescriptor>> map = initNSDescriptorsMap();
final CachedValue<XmlNSDescriptor> descriptor = map.get(namespace);
if (descriptor != null) {
final XmlNSDescriptor value = descriptor.getValue();
if (value != null) {
return value;
}
}
if (parentTag == null) {
final XmlDocument parentOfType = PsiTreeUtil.getParentOfType(this, XmlDocument.class);
if (parentOfType == null) {
return null;
}
return parentOfType.getDefaultNSDescriptor(namespace, strict);
}
return parentTag.getNSDescriptor(namespace, strict);
}
@Override
public boolean isEmpty() {
return XmlChildRole.CLOSING_TAG_START_FINDER.findChild(this) == null;
}
@Override
public void collapseIfEmpty() {
final XmlTag[] tags = getSubTags();
if (tags.length > 0) {
return;
}
final ASTNode closingName = XmlChildRole.CLOSING_TAG_NAME_FINDER.findChild(this);
final ASTNode startTagEnd = XmlChildRole.START_TAG_END_FINDER.findChild(this);
if (closingName == null || startTagEnd == null) {
return;
}
final PomModel pomModel = PomManager.getModel(getProject());
final PomTransactionBase transaction = new PomTransactionBase(this, pomModel.getModelAspect(XmlAspect.class)) {
@Override
@Nullable
public PomModelEvent runInner() {
final ASTNode closingBracket = closingName.getTreeNext();
removeRange(startTagEnd, closingBracket);
final LeafElement emptyTagEnd = Factory.createSingleLeafElement(XmlTokenType.XML_EMPTY_ELEMENT_END, "/>", 0, 2, null, getManager());
replaceChild(closingBracket, emptyTagEnd);
return null;
}
};
try {
pomModel.runTransaction(transaction);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
@Override
@Nullable
@NonNls
public String getSubTagText(@NonNls String qname) {
final XmlTag tag = findFirstSubTag(qname);
if (tag == null) return null;
return tag.getValue().getText();
}
protected final Map<String, CachedValue<XmlNSDescriptor>> initNSDescriptorsMap() {
Map<String, CachedValue<XmlNSDescriptor>> map = myNSDescriptorsMap;
if (map == null) {
RecursionGuard.StackStamp stamp = ourGuard.markStack();
map = computeNsDescriptorMap();
if (stamp.mayCacheNow()) {
myNSDescriptorsMap = map;
}
}
return map;
}
@NotNull
private Map<String, CachedValue<XmlNSDescriptor>> computeNsDescriptorMap() {
Map<String, CachedValue<XmlNSDescriptor>> map = null;
// XSD aware attributes processing
final String noNamespaceDeclaration = getAttributeValue("noNamespaceSchemaLocation", XmlUtil.XML_SCHEMA_INSTANCE_URI);
final String schemaLocationDeclaration = getAttributeValue("schemaLocation", XmlUtil.XML_SCHEMA_INSTANCE_URI);
if (noNamespaceDeclaration != null) {
map = initializeSchema(XmlUtil.EMPTY_URI, null, noNamespaceDeclaration, null);
}
if (schemaLocationDeclaration != null) {
final StringTokenizer tokenizer = new StringTokenizer(schemaLocationDeclaration);
while (tokenizer.hasMoreTokens()) {
final String uri = tokenizer.nextToken();
if (tokenizer.hasMoreTokens()) {
map = initializeSchema(uri, getNSVersion(uri, this), tokenizer.nextToken(), map);
}
}
}
// namespace attributes processing (XSD declaration via ExternalResourceManager)
if (hasNamespaceDeclarations()) {
for (final XmlAttribute attribute : getAttributes()) {
if (attribute.isNamespaceDeclaration()) {
String ns = attribute.getValue();
if (ns == null) ns = XmlUtil.EMPTY_URI;
ns = getRealNs(ns);
if (map == null || !map.containsKey(ns)) {
map = initializeSchema(ns, getNSVersion(ns, this), getNsLocation(ns), map);
}
}
}
}
return map == null ? Collections.<String, CachedValue<XmlNSDescriptor>>emptyMap() : map;
}
private Map<String, CachedValue<XmlNSDescriptor>> initializeSchema(@NotNull final String namespace,
@Nullable final String version,
final String fileLocation,
Map<String, CachedValue<XmlNSDescriptor>> map) {
if (map == null) map = new THashMap<String, CachedValue<XmlNSDescriptor>>();
// We put cached value in any case to cause its value update on e.g. mapping change
map.put(namespace, CachedValuesManager.getManager(getManager().getProject()).createCachedValue(new CachedValueProvider<XmlNSDescriptor>() {
@Override
public Result<XmlNSDescriptor> compute() {
XmlNSDescriptor descriptor = getImplicitNamespaceDescriptor(fileLocation);
if (descriptor != null) {
return new Result<XmlNSDescriptor>(descriptor, ArrayUtil.append(descriptor.getDependences(), XmlTagImpl.this));
}
XmlFile currentFile = retrieveFile(fileLocation, version, namespace);
if (currentFile == null) {
final XmlDocument document = XmlUtil.getContainingFile(XmlTagImpl.this).getDocument();
if (document != null) {
final String uri = XmlUtil.getDtdUri(document);
if (uri != null) {
final XmlFile containingFile = XmlUtil.getContainingFile(document);
final XmlFile xmlFile = XmlUtil.findNamespace(containingFile, uri);
descriptor = xmlFile == null ? null : (XmlNSDescriptor)xmlFile.getDocument().getMetaData();
}
// We want to get fixed xmlns attr from dtd and check its default with requested namespace
if (descriptor instanceof com.intellij.xml.impl.dtd.XmlNSDescriptorImpl) {
final XmlElementDescriptor elementDescriptor = descriptor.getElementDescriptor(XmlTagImpl.this);
if (elementDescriptor != null) {
final XmlAttributeDescriptor attributeDescriptor = elementDescriptor.getAttributeDescriptor("xmlns", XmlTagImpl.this);
if (attributeDescriptor != null && attributeDescriptor.isFixed()) {
final String defaultValue = attributeDescriptor.getDefaultValue();
if (defaultValue != null && defaultValue.equals(namespace)) {
return new Result<XmlNSDescriptor>(descriptor, descriptor.getDependences(), XmlTagImpl.this,
ExternalResourceManager.getInstance());
}
}
}
}
}
}
PsiMetaOwner currentOwner = retrieveOwner(currentFile, namespace);
if (currentOwner != null) {
descriptor = (XmlNSDescriptor)currentOwner.getMetaData();
if (descriptor != null) {
return new Result<XmlNSDescriptor>(descriptor, descriptor.getDependences(), XmlTagImpl.this,
ExternalResourceManager.getInstance());
}
}
return new Result<XmlNSDescriptor>(null, XmlTagImpl.this, currentFile == null ? XmlTagImpl.this : currentFile, ExternalResourceManager.getInstance());
}
}, false));
return map;
}
@Nullable
private XmlNSDescriptor getImplicitNamespaceDescriptor(String ns) {
PsiFile file = getContainingFile();
if (file == null) return null;
Module module = ModuleUtilCore.findModuleForPsiElement(file);
if (module != null) {
for (ImplicitNamespaceDescriptorProvider provider : Extensions.getExtensions(ImplicitNamespaceDescriptorProvider.EP_NAME)) {
XmlNSDescriptor nsDescriptor = provider.getNamespaceDescriptor(module, ns, file);
if (nsDescriptor != null) return nsDescriptor;
}
}
return null;
}
@Nullable
private XmlFile retrieveFile(final String fileLocation, final String version, String namespace) {
final String targetNs = XmlUtil.getTargetSchemaNsFromTag(this);
if (fileLocation.equals(targetNs)) {
return null;
}
final XmlFile file = XmlUtil.getContainingFile(this);
final PsiFile psiFile = ExternalResourceManager.getInstance().getResourceLocation(fileLocation, file, version);
if (psiFile instanceof XmlFile) {
return (XmlFile)psiFile;
}
return XmlNamespaceIndex.guessSchema(namespace, myLocalName, version, file);
}
@Nullable
private PsiMetaOwner retrieveOwner(final XmlFile file, @NotNull final String namespace) {
if (file == null) {
return namespace.equals(XmlUtil.getTargetSchemaNsFromTag(this)) ? this : null;
}
return file.getDocument();
}
@Override
public PsiReference getReference() {
return ArrayUtil.getFirstElement(getReferences());
}
@Override
public XmlElementDescriptor getDescriptor() {
final long curModCount = getManager().getModificationTracker().getModificationCount();
long curExtResourcesModCount = ExternalResourceManagerEx.getInstanceEx().getModificationCount(getProject());
if (myDescriptorModCount != curModCount || myExtResourcesModCount != curExtResourcesModCount) {
if (myExtResourcesModCount != curExtResourcesModCount) {
myNSDescriptorsMap = null;
}
RecursionGuard.StackStamp stamp = ourGuard.markStack();
XmlElementDescriptor descriptor = computeElementDescriptor();
if (!stamp.mayCacheNow()) {
return descriptor;
}
myCachedDescriptor = descriptor;
myDescriptorModCount = curModCount;
myExtResourcesModCount = curExtResourcesModCount;
}
return myCachedDescriptor;
}
@Nullable
protected XmlElementDescriptor computeElementDescriptor() {
for (XmlElementDescriptorProvider provider : Extensions.getExtensions(XmlElementDescriptorProvider.EP_NAME)) {
XmlElementDescriptor elementDescriptor = provider.getDescriptor(this);
if (elementDescriptor != null) {
return elementDescriptor;
}
}
final String namespace = getNamespace();
if (XmlUtil.EMPTY_URI.equals(namespace)) { //nonqualified items
final XmlTag parent = getParentTag();
if (parent != null) {
final XmlElementDescriptor descriptor = parent.getDescriptor();
if (descriptor != null) {
XmlElementDescriptor fromParent = descriptor.getElementDescriptor(this, parent);
if (fromParent != null && !(fromParent instanceof AnyXmlElementDescriptor)) {
return fromParent;
}
}
}
}
XmlElementDescriptor elementDescriptor = null;
final XmlNSDescriptor nsDescriptor = getNSDescriptor(namespace, false);
if (LOG.isDebugEnabled()) {
LOG.debug(
"Descriptor for namespace " + namespace + " is " + (nsDescriptor != null ? nsDescriptor.getClass().getCanonicalName() : "NULL"));
}
if (nsDescriptor != null) {
if (!DumbService.getInstance(getProject()).isDumb() || DumbService.isDumbAware(nsDescriptor)) {
elementDescriptor = nsDescriptor.getElementDescriptor(this);
}
}
if (elementDescriptor == null) {
return XmlUtil.findXmlDescriptorByType(this);
}
return elementDescriptor;
}
@Override
public int getChildRole(ASTNode child) {
LOG.assertTrue(child.getTreeParent() == this);
IElementType i = child.getElementType();
if (i == XmlTokenType.XML_NAME || i == XmlTokenType.XML_TAG_NAME) {
return XmlChildRole.XML_TAG_NAME;
}
else if (i == XmlElementType.XML_ATTRIBUTE) {
return XmlChildRole.XML_ATTRIBUTE;
}
else {
return ChildRoleBase.NONE;
}
}
@Override
@NotNull
public String getName() {
String name = myName;
if (name == null) {
final ASTNode nameElement = XmlChildRole.START_TAG_NAME_FINDER.findChild(this);
if (nameElement != null) {
name = nameElement.getText();
}
else {
name = "";
}
myName = name;
}
return name;
}
@Override
public PsiElement setName(@NotNull final String name) throws IncorrectOperationException {
final PomModel model = PomManager.getModel(getProject());
final XmlAspect aspect = model.getModelAspect(XmlAspect.class);
model.runTransaction(new PomTransactionBase(this, aspect) {
@Override
public PomModelEvent runInner() throws IncorrectOperationException {
final String oldName = getName();
final XmlTagImpl dummyTag =
(XmlTagImpl)XmlElementFactory.getInstance(getProject()).createTagFromText(XmlTagUtil.composeTagText(name, "aa"));
final XmlTagImpl tag = XmlTagImpl.this;
final CharTable charTableByTree = SharedImplUtil.findCharTableByTree(tag);
ASTNode child = XmlChildRole.START_TAG_NAME_FINDER.findChild(tag);
LOG.assertTrue(child != null, "It seems '" + name + "' is not a valid tag name");
TreeElement tagElement = (TreeElement)XmlChildRole.START_TAG_NAME_FINDER.findChild(dummyTag);
LOG.assertTrue(tagElement != null, "What's wrong with it? '" + name + "'");
tag.replaceChild(child, ChangeUtil.copyElement(tagElement, charTableByTree));
final ASTNode childByRole = XmlChildRole.CLOSING_TAG_NAME_FINDER.findChild(tag);
if (childByRole != null) {
final TreeElement treeElement = (TreeElement)XmlChildRole.CLOSING_TAG_NAME_FINDER.findChild(dummyTag);
if (treeElement != null) {
tag.replaceChild(childByRole, ChangeUtil.copyElement(treeElement, charTableByTree));
}
}
return XmlTagNameChangedImpl.createXmlTagNameChanged(model, tag, oldName);
}
});
return this;
}
@Override
@NotNull
public XmlAttribute[] getAttributes() {
XmlAttribute[] attributes = myAttributes;
if (attributes == null) {
Map<String, String> attributesValueMap = new THashMap<String, String>();
attributes = calculateAttributes(attributesValueMap);
myAttributeValueMap = attributesValueMap;
myAttributes = attributes;
}
return attributes;
}
@NotNull
private XmlAttribute[] calculateAttributes(final Map<String, String> attributesValueMap) {
final List<XmlAttribute> result = new ArrayList<XmlAttribute>(10);
processChildren(new PsiElementProcessor() {
@Override
public boolean execute(@NotNull PsiElement element) {
if (element instanceof XmlAttribute) {
XmlAttribute attribute = (XmlAttribute)element;
result.add(attribute);
cacheOneAttributeValue(attribute.getName(), attribute.getValue(), attributesValueMap);
myHasNamespaceDeclarations = myHasNamespaceDeclarations || attribute.isNamespaceDeclaration();
}
else if (element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_TAG_END) {
return false;
}
return true;
}
});
if (result.isEmpty()) {
return XmlAttribute.EMPTY_ARRAY;
}
else {
return ContainerUtil.toArray(result, new XmlAttribute[result.size()]);
}
}
protected void cacheOneAttributeValue(String name, String value, final Map<String, String> attributesValueMap) {
attributesValueMap.put(name, value);
}
@Override
public String getAttributeValue(String qname) { //todo ?
Map<String, String> map = myAttributeValueMap;
while (map == null) {
getAttributes();
map = myAttributeValueMap;
if (map == null) {
myAttributes = null;
}
}
return map.get(qname);
}
@Override
public String getAttributeValue(String _name, String namespace) {
if (namespace == null) {
return getAttributeValue(_name);
}
XmlTagImpl current = this;
PsiElement parent = getParent();
while (current != null) {
BidirectionalMap<String, String> map = current.initNamespaceMaps(parent);
if (map != null) {
List<String> keysByValue = map.getKeysByValue(namespace);
if (keysByValue != null && !keysByValue.isEmpty()) {
for (String prefix : keysByValue) {
if (prefix != null && !prefix.isEmpty()) {
final String value = getAttributeValue(prefix + ":" + _name);
if (value != null) return value;
}
}
}
}
current = parent instanceof XmlTag ? (XmlTagImpl)parent : null;
parent = parent.getParent();
}
if (namespace.isEmpty() || getNamespace().equals(namespace)) {
return getAttributeValue(_name);
}
return null;
}
@Override
@NotNull
public XmlTag[] getSubTags() {
return CachedValuesManager.getManager(getProject()).getParameterizedCachedValue(this, SUBTAGS_KEY, CACHED_VALUE_PROVIDER, false, this);
}
protected void fillSubTags(final List<XmlTag> result) {
processElements(new PsiElementProcessor() {
@Override
public boolean execute(@NotNull PsiElement element) {
if (element instanceof XmlTag) {
assert element.isValid();
result.add((XmlTag)element);
}
return true;
}
}, this);
}
@Override
@NotNull
public XmlTag[] findSubTags(String name) {
return findSubTags(name, null);
}
@Override
@NotNull
public XmlTag[] findSubTags(final String name, @Nullable final String namespace) {
final XmlTag[] subTags = getSubTags();
final List<XmlTag> result = new ArrayList<XmlTag>();
for (final XmlTag subTag : subTags) {
if (namespace == null) {
if (name.equals(subTag.getName())) result.add(subTag);
}
else if (name.equals(subTag.getLocalName()) && namespace.equals(subTag.getNamespace())) {
result.add(subTag);
}
}
return ContainerUtil.toArray(result, new XmlTag[result.size()]);
}
@Override
public XmlTag findFirstSubTag(String name) {
final XmlTag[] subTags = findSubTags(name);
if (subTags.length > 0) return subTags[0];
return null;
}
@Override
public XmlAttribute getAttribute(String name, String namespace) {
if (name != null && name.indexOf(':') != -1 ||
namespace == null ||
XmlUtil.EMPTY_URI.equals(namespace) ||
XmlUtil.ANY_URI.equals(namespace)) {
return getAttribute(name);
}
final String prefix = getPrefixByNamespace(namespace);
if (prefix == null || prefix.isEmpty()) return null;
return getAttribute(prefix + ":" + name);
}
@Override
@Nullable
public XmlAttribute getAttribute(String qname) {
if (qname == null) return null;
final XmlAttribute[] attributes = getAttributes();
final boolean caseSensitive = isCaseSensitive();
for (final XmlAttribute attribute : attributes) {
final LeafElement attrNameElement = (LeafElement)XmlChildRole.ATTRIBUTE_NAME_FINDER.findChild(attribute.getNode());
if (attrNameElement != null &&
(caseSensitive && Comparing.equal(attrNameElement.getChars(), qname) ||
!caseSensitive && Comparing.equal(attrNameElement.getChars(), qname, false))) {
return attribute;
}
}
return null;
}
protected boolean isCaseSensitive() {
return true;
}
@Override
@NotNull
public String getNamespace() {
String cachedNamespace = myCachedNamespace;
final long curModCount = getManager().getModificationTracker().getModificationCount();
if (cachedNamespace != null && myModCount == curModCount) {
return cachedNamespace;
}
RecursionGuard.StackStamp stamp = ourGuard.markStack();
cachedNamespace = getNamespaceByPrefix(getNamespacePrefix());
if (!stamp.mayCacheNow()) {
return cachedNamespace;
}
myCachedNamespace = cachedNamespace;
myModCount = curModCount;
return cachedNamespace;
}
@Override
@NotNull
public String getNamespacePrefix() {
return XmlUtil.findPrefixByQualifiedName(getName());
}
@Override
@NotNull
public String getNamespaceByPrefix(String prefix) {
final PsiElement parent = getParent();
if (!parent.isValid()) {
LOG.error(this.isValid());
}
BidirectionalMap<String, String> map = initNamespaceMaps(parent);
if (map != null) {
final String ns = map.get(prefix);
if (ns != null) return ns;
}
if (parent instanceof XmlTag) return ((XmlTag)parent).getNamespaceByPrefix(prefix);
//The prefix 'xml' is by definition bound to the namespace name http://www.w3.org/XML/1998/namespace. It MAY, but need not, be declared
if (XML_NS_PREFIX.equals(prefix)) return XmlUtil.XML_NAMESPACE_URI;
if (!prefix.isEmpty() &&
!hasNamespaceDeclarations() &&
getNamespacePrefix().equals(prefix)) {
// When there is no namespace declarations then qualified names should be just used in dtds
// this implies that we may have "" namespace prefix ! (see last paragraph in Namespaces in Xml, Section 5)
String result = ourGuard.doPreventingRecursion("getNsByPrefix", true, new Computable<String>() {
@Override
public String compute() {
final String nsFromEmptyPrefix = getNamespaceByPrefix("");
final XmlNSDescriptor nsDescriptor = getNSDescriptor(nsFromEmptyPrefix, false);
final XmlElementDescriptor descriptor = nsDescriptor != null ? nsDescriptor.getElementDescriptor(XmlTagImpl.this) : null;
final String nameFromRealDescriptor =
descriptor != null && descriptor.getDeclaration() != null && descriptor.getDeclaration().isPhysical()
? descriptor.getName()
: "";
if (nameFromRealDescriptor.equals(getName())) return nsFromEmptyPrefix;
return XmlUtil.EMPTY_URI;
}
});
if (result != null) {
return result;
}
}
return XmlUtil.EMPTY_URI;
}
@Override
public String getPrefixByNamespace(String namespace) {
final PsiElement parent = getParent();
BidirectionalMap<String, String> map = initNamespaceMaps(parent);
if (map != null) {
List<String> keysByValue = map.getKeysByValue(namespace);
final String ns = keysByValue == null || keysByValue.isEmpty() ? null : keysByValue.get(0);
if (ns != null) return ns;
}
if (parent instanceof XmlTag) return ((XmlTag)parent).getPrefixByNamespace(namespace);
//The prefix 'xml' is by definition bound to the namespace name http://www.w3.org/XML/1998/namespace. It MAY, but need not, be declared
if (XmlUtil.XML_NAMESPACE_URI.equals(namespace)) return XML_NS_PREFIX;
return null;
}
@Override
public String[] knownNamespaces() {
final PsiElement parentElement = getParent();
BidirectionalMap<String, String> map = initNamespaceMaps(parentElement);
Set<String> known = Collections.emptySet();
if (map != null) {
known = new HashSet<String>(map.values());
}
if (parentElement instanceof XmlTag) {
if (known.isEmpty()) return ((XmlTag)parentElement).knownNamespaces();
ContainerUtil.addAll(known, ((XmlTag)parentElement).knownNamespaces());
}
else {
XmlExtension xmlExtension = XmlExtension.getExtensionByElement(this);
if (xmlExtension != null) {
final XmlFile xmlFile = xmlExtension.getContainingFile(this);
if (xmlFile != null) {
final XmlTag rootTag = xmlFile.getRootTag();
if (rootTag != null && rootTag != this) {
if (known.isEmpty()) return rootTag.knownNamespaces();
ContainerUtil.addAll(known, rootTag.knownNamespaces());
}
}
}
}
return ArrayUtil.toStringArray(known);
}
@Nullable
private BidirectionalMap<String, String> initNamespaceMaps(PsiElement parent) {
BidirectionalMap<String, String> map = myNamespaceMap;
if (map == null) {
RecursionGuard.StackStamp stamp = ourGuard.markStack();
map = computeNamespaceMap(parent);
if (stamp.mayCacheNow()) {
myNamespaceMap = map;
}
}
return map;
}
@Nullable
private BidirectionalMap<String, String> computeNamespaceMap(PsiElement parent) {
BidirectionalMap<String, String> map = null;
boolean hasNamespaceDeclarations = hasNamespaceDeclarations();
if (hasNamespaceDeclarations) {
map = new BidirectionalMap<String, String>();
final XmlAttribute[] attributes = getAttributes();
for (final XmlAttribute attribute : attributes) {
if (attribute.isNamespaceDeclaration()) {
final String name = attribute.getName();
int splitIndex = name.indexOf(':');
final String value = getRealNs(attribute.getValue());
if (value != null) {
if (splitIndex < 0) {
map.put("", value);
}
else {
map.put(XmlUtil.findLocalNameByQualifiedName(name), value);
}
}
}
}
}
if (parent instanceof XmlDocument) {
final XmlExtension extension = XmlExtension.getExtensionByElement(parent);
if (extension != null) {
final String[][] namespacesFromDocument = extension.getNamespacesFromDocument((XmlDocument)parent, hasNamespaceDeclarations);
if (namespacesFromDocument != null) {
if (map == null) {
map = new BidirectionalMap<String, String>();
}
for (final String[] prefix2ns : namespacesFromDocument) {
map.put(prefix2ns[0], getRealNs(prefix2ns[1]));
}
}
}
}
return map;
}
private String getNsLocation(String ns) {
if (XmlUtil.XHTML_URI.equals(ns)) {
return XmlUtil.getDefaultXhtmlNamespace(getProject());
}
if (XmlNSDescriptorImpl.equalsToSchemaName(this, XmlNSDescriptorImpl.SCHEMA_TAG_NAME)) {
for (XmlTag subTag : getSubTags()) {
if (XmlNSDescriptorImpl.equalsToSchemaName(subTag, XmlNSDescriptorImpl.IMPORT_TAG_NAME) &&
ns.equals(subTag.getAttributeValue("namespace"))) {
String location = subTag.getAttributeValue("schemaLocation");
if (location != null) {
return location;
}
}
}
}
return XmlUtil.getSchemaLocation(this, ns);
}
protected String getRealNs(final String value) {
return value;
}
@Override
@NotNull
public String getLocalName() {
String localName = myLocalName;
if (localName == null) {
final String name = getName();
myLocalName = localName = name.substring(name.indexOf(':') + 1);
}
return localName;
}
@Override
public boolean hasNamespaceDeclarations() {
getAttributes();
return myHasNamespaceDeclarations;
}
@Override
@NotNull
public Map<String, String> getLocalNamespaceDeclarations() {
Map<String, String> namespaces = new THashMap<String, String>();
for (final XmlAttribute attribute : getAttributes()) {
if (!attribute.isNamespaceDeclaration() || attribute.getValue() == null) continue;
// xmlns -> "", xmlns:a -> a
final String localName = attribute.getLocalName();
namespaces.put(localName.equals(attribute.getName()) ? "" : localName, attribute.getValue());
}
return namespaces;
}
@Override
public XmlAttribute setAttribute(String qname, String value) throws IncorrectOperationException {
final XmlAttribute attribute = getAttribute(qname);
if (attribute != null) {
if (value == null) {
deleteChildInternal(attribute.getNode());
return null;
}
attribute.setValue(value);
return attribute;
}
else if (value == null) {
return null;
}
else {
PsiElement xmlAttribute = add(XmlElementFactory.getInstance(getProject()).createXmlAttribute(qname, value));
while (!(xmlAttribute instanceof XmlAttribute)) xmlAttribute = xmlAttribute.getNextSibling();
return (XmlAttribute)xmlAttribute;
}
}
@Override
public XmlAttribute setAttribute(String name, String namespace, String value) throws IncorrectOperationException {
if (!Comparing.equal(namespace, "")) {
final String prefix = getPrefixByNamespace(namespace);
if (prefix != null && !prefix.isEmpty()) name = prefix + ":" + name;
}
return setAttribute(name, value);
}
@Override
public XmlTag createChildTag(String localName, String namespace, String bodyText, boolean enforceNamespacesDeep) {
return XmlUtil.createChildTag(this, localName, namespace, bodyText, enforceNamespacesDeep);
}
@Override
public XmlTag addSubTag(XmlTag subTag, boolean first) {
XmlTagChild[] children = getSubTags();
if (children.length == 0) {
children = getValue().getChildren();
}
if (children.length == 0) {
return (XmlTag)add(subTag);
}
else if (first) {
return (XmlTag)addBefore(subTag, children[0]);
}
else {
return (XmlTag)addAfter(subTag, ArrayUtil.getLastElement(children));
}
}
@Override
@NotNull
public XmlTagValue getValue() {
XmlTagValue tagValue = myValue;
if (tagValue == null) {
myValue = tagValue = XmlTagValueImpl.createXmlTagValue(this);
}
return tagValue;
}
@Override
public void accept(@NotNull PsiElementVisitor visitor) {
if (visitor instanceof XmlElementVisitor) {
((XmlElementVisitor)visitor).visitXmlTag(this);
}
else {
visitor.visitElement(this);
}
}
public String toString() {
return "XmlTag:" + getName();
}
@Override
public PsiMetaData getMetaData() {
return MetaRegistry.getMeta(this);
}
@Override
public TreeElement addInternal(TreeElement first, ASTNode last, ASTNode anchor, Boolean beforeB) {
TreeElement firstAppended = null;
boolean before = beforeB == null || beforeB.booleanValue();
try {
TreeElement next;
do {
next = first.getTreeNext();
if (firstAppended == null) {
firstAppended = addInternal(first, anchor, before);
anchor = firstAppended;
}
else {
anchor = addInternal(first, anchor, false);
}
}
while (first != last && (first = next) != null);
}
catch (IncorrectOperationException ignored) {
}
finally {
clearCaches();
}
return firstAppended;
}
private TreeElement addInternal(TreeElement child, ASTNode anchor, boolean before) throws IncorrectOperationException {
final PomModel model = PomManager.getModel(getProject());
if (anchor != null && child.getElementType() == XmlElementType.XML_TEXT) {
XmlText psi = null;
if (anchor.getPsi() instanceof XmlText) {
psi = (XmlText)anchor.getPsi();
}
else {
final ASTNode other = before ? anchor.getTreePrev() : anchor.getTreeNext();
if (other != null && other.getPsi() instanceof XmlText) {
before = !before;
psi = (XmlText)other.getPsi();
}
}
if (psi != null) {
if (before) {
psi.insertText(((XmlText)child.getPsi()).getValue(), 0);
}
else {
psi.insertText(((XmlText)child.getPsi()).getValue(), psi.getValue().length());
}
return (TreeElement)psi.getNode();
}
}
LOG.assertTrue(child.getPsi() instanceof XmlAttribute || child.getPsi() instanceof XmlTagChild);
final InsertTransaction transaction;
if (child.getElementType() == XmlElementType.XML_ATTRIBUTE) {
transaction = new InsertAttributeTransaction(child, anchor, before, model);
}
else if (anchor == null) {
transaction = getBodyInsertTransaction(child);
}
else {
transaction = new GenericInsertTransaction(child, anchor, before);
}
model.runTransaction(transaction);
return transaction.getFirstInserted();
}
protected InsertTransaction getBodyInsertTransaction(final TreeElement child) {
return new BodyInsertTransaction(child);
}
@Override
public void deleteChildInternal(@NotNull final ASTNode child) {
final PomModel model = PomManager.getModel(getProject());
final XmlAspect aspect = model.getModelAspect(XmlAspect.class);
if (child.getElementType() == XmlElementType.XML_ATTRIBUTE) {
try {
model.runTransaction(new PomTransactionBase(this, aspect) {
@Override
public PomModelEvent runInner() {
final String name = ((XmlAttribute)child).getName();
XmlTagImpl.super.deleteChildInternal(child);
return XmlAttributeSetImpl.createXmlAttributeSet(model, XmlTagImpl.this, name, null);
}
});
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
else {
final ASTNode treePrev = child.getTreePrev();
final ASTNode treeNext = child.getTreeNext();
XmlTagImpl.super.deleteChildInternal(child);
if (treePrev != null &&
treeNext != null &&
treePrev.getElementType() == XmlElementType.XML_TEXT &&
treeNext.getElementType() == XmlElementType.XML_TEXT) {
final XmlText prevText = (XmlText)treePrev.getPsi();
final XmlText nextText = (XmlText)treeNext.getPsi();
try {
prevText.setValue(prevText.getValue() + nextText.getValue());
nextText.delete();
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
}
}
private ASTNode expandTag() throws IncorrectOperationException {
ASTNode endTagStart = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(this);
if (endTagStart == null) {
final XmlTagImpl tagFromText =
(XmlTagImpl)XmlElementFactory.getInstance(getProject()).createTagFromText("<" + getName() + "></" + getName() + ">");
final ASTNode startTagStart = XmlChildRole.START_TAG_END_FINDER.findChild(tagFromText);
endTagStart = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(tagFromText);
final LeafElement emptyTagEnd = (LeafElement)XmlChildRole.EMPTY_TAG_END_FINDER.findChild(this);
if (emptyTagEnd != null) removeChild(emptyTagEnd);
addChildren(startTagStart, null, null);
}
return endTagStart;
}
@Override
public XmlTag getParentTag() {
final PsiElement parent = getParent();
if (parent instanceof XmlTag) return (XmlTag)parent;
return null;
}
@Override
public XmlTagChild getNextSiblingInTag() {
final PsiElement nextSibling = getNextSibling();
if (nextSibling instanceof XmlTagChild) return (XmlTagChild)nextSibling;
return null;
}
@Override
public XmlTagChild getPrevSiblingInTag() {
final PsiElement prevSibling = getPrevSibling();
if (prevSibling instanceof XmlTagChild) return (XmlTagChild)prevSibling;
return null;
}
@Override
public Icon getElementIcon(int flags) {
return PlatformIcons.XML_TAG_ICON;
}
protected class BodyInsertTransaction extends InsertTransaction {
private final TreeElement myChild;
private ASTNode myNewElement;
public BodyInsertTransaction(TreeElement child) {
super(XmlTagImpl.this);
myChild = child;
}
@Override
public PomModelEvent runInner() throws IncorrectOperationException {
final ASTNode anchor = expandTag();
if (myChild.getElementType() == XmlElementType.XML_TAG) {
// compute where to insert tag according to DTD or XSD
final XmlElementDescriptor parentDescriptor = getDescriptor();
final XmlTag[] subTags = getSubTags();
final PsiElement declaration = parentDescriptor != null ? parentDescriptor.getDeclaration() : null;
// filtering out generated dtds
if (declaration != null &&
declaration.getContainingFile() != null &&
declaration.getContainingFile().isPhysical() &&
subTags.length > 0) {
final XmlElementDescriptor[] childElementDescriptors = parentDescriptor.getElementsDescriptors(XmlTagImpl.this);
int subTagNum = -1;
for (final XmlElementDescriptor childElementDescriptor : childElementDescriptors) {
final String childElementName = childElementDescriptor.getName();
while (subTagNum < subTags.length - 1 && subTags[subTagNum + 1].getName().equals(childElementName)) {
subTagNum++;
}
if (childElementName.equals(XmlChildRole.START_TAG_NAME_FINDER.findChild(myChild).getText())) {
// insert child just after anchor
// insert into the position specified by index
if (subTagNum >= 0) {
final ASTNode subTag = (ASTNode)subTags[subTagNum];
if (subTag.getTreeParent() != XmlTagImpl.this) {
// in entity
final XmlEntityRef entityRef = PsiTreeUtil.getParentOfType(subTags[subTagNum], XmlEntityRef.class);
throw new IncorrectOperationException(
"Can't insert subtag to the entity. Entity reference text: " + (entityRef == null ? "" : entityRef.getText()));
}
myNewElement = XmlTagImpl.super.addInternal(myChild, myChild, subTag, Boolean.FALSE);
}
else {
final ASTNode child = XmlChildRole.START_TAG_END_FINDER.findChild(XmlTagImpl.this);
myNewElement = XmlTagImpl.super.addInternal(myChild, myChild, child, Boolean.FALSE);
}
return null;
}
}
}
else {
final ASTNode child = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(XmlTagImpl.this);
myNewElement = XmlTagImpl.super.addInternal(myChild, myChild, child, Boolean.TRUE);
return null;
}
}
myNewElement = XmlTagImpl.super.addInternal(myChild, myChild, anchor, Boolean.TRUE);
return null;
}
@Override
public TreeElement getFirstInserted() {
return (TreeElement)myNewElement;
}
}
protected class InsertAttributeTransaction extends InsertTransaction {
private final TreeElement myChild;
private final ASTNode myAnchor;
private final boolean myBefore;
private final PomModel myModel;
private TreeElement myFirstInserted = null;
public InsertAttributeTransaction(final TreeElement child, final ASTNode anchor, final boolean before, final PomModel model) {
super(XmlTagImpl.this);
myChild = child;
myAnchor = anchor;
myBefore = before;
myModel = model;
}
@Override
public PomModelEvent runInner() {
final String value = ((XmlAttribute)myChild).getValue();
final String name = ((XmlAttribute)myChild).getName();
if (myAnchor == null) {
ASTNode startTagEnd = XmlChildRole.START_TAG_END_FINDER.findChild(XmlTagImpl.this);
if (startTagEnd == null) startTagEnd = XmlChildRole.EMPTY_TAG_END_FINDER.findChild(XmlTagImpl.this);
if (startTagEnd == null) {
ASTNode anchor = getLastChildNode();
while (anchor instanceof PsiWhiteSpace) {
anchor = anchor.getTreePrev();
}
if (anchor instanceof PsiErrorElement) {
final LeafElement token = Factory
.createSingleLeafElement(XmlTokenType.XML_EMPTY_ELEMENT_END, "/>", 0, 2, SharedImplUtil.findCharTableByTree(anchor),
getManager());
replaceChild(anchor, token);
startTagEnd = token;
}
}
if (startTagEnd == null) {
ASTNode anchor = XmlChildRole.START_TAG_NAME_FINDER.findChild(XmlTagImpl.this);
myFirstInserted = XmlTagImpl.super.addInternal(myChild, myChild, anchor, Boolean.FALSE);
}
else {
myFirstInserted = XmlTagImpl.super.addInternal(myChild, myChild, startTagEnd, Boolean.TRUE);
}
}
else {
myFirstInserted = XmlTagImpl.super.addInternal(myChild, myChild, myAnchor, Boolean.valueOf(myBefore));
}
return XmlAttributeSetImpl.createXmlAttributeSet(myModel, XmlTagImpl.this, name, value);
}
@Override
public TreeElement getFirstInserted() {
return myFirstInserted;
}
}
protected class GenericInsertTransaction extends InsertTransaction {
private final TreeElement myChild;
private final ASTNode myAnchor;
private final boolean myBefore;
private TreeElement myRetHolder;
public GenericInsertTransaction(final TreeElement child, final ASTNode anchor, final boolean before) {
super(XmlTagImpl.this);
myChild = child;
myAnchor = anchor;
myBefore = before;
}
@Override
public PomModelEvent runInner() {
myRetHolder = XmlTagImpl.super.addInternal(myChild, myChild, myAnchor, Boolean.valueOf(myBefore));
return null;
}
@Override
public TreeElement getFirstInserted() {
return myRetHolder;
}
}
protected abstract class InsertTransaction extends PomTransactionBase {
public InsertTransaction(final PsiElement scope) {
super(scope, PomManager.getModel(getProject()).getModelAspect(XmlAspect.class));
}
public abstract TreeElement getFirstInserted();
}
}