| /* |
| * 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.psi.impl.source.xml; |
| |
| import com.intellij.javaee.ExternalResourceManager; |
| import com.intellij.javaee.ExternalResourceManagerEx; |
| import com.intellij.lang.ASTNode; |
| import com.intellij.lang.dtd.DTDLanguage; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.progress.ProcessCanceledException; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.vfs.VirtualFile; |
| 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.XmlDocumentChangedImpl; |
| import com.intellij.psi.*; |
| import com.intellij.psi.impl.PsiCachedValueImpl; |
| import com.intellij.psi.impl.meta.MetaRegistry; |
| import com.intellij.psi.impl.source.html.dtd.HtmlNSDescriptorImpl; |
| import com.intellij.psi.impl.source.tree.CompositePsiElement; |
| import com.intellij.psi.impl.source.tree.TreeElement; |
| import com.intellij.psi.meta.PsiMetaData; |
| import com.intellij.psi.meta.PsiMetaOwner; |
| import com.intellij.psi.tree.ChildRoleBase; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.util.CachedValue; |
| import com.intellij.psi.util.CachedValueProvider; |
| import com.intellij.psi.xml.*; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.containers.ConcurrentHashMap; |
| import com.intellij.xml.Html5SchemaProvider; |
| import com.intellij.xml.XmlExtension; |
| import com.intellij.xml.XmlNSDescriptor; |
| import com.intellij.xml.index.XmlNamespaceIndex; |
| import com.intellij.xml.util.XmlNSDescriptorSequence; |
| import com.intellij.xml.util.XmlUtil; |
| import gnu.trove.TObjectIntHashMap; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| * @author Mike |
| */ |
| public class XmlDocumentImpl extends XmlElementImpl implements XmlDocument { |
| |
| private static final Key<Boolean> AUTO_GENERATED = Key.create("auto-generated xml schema"); |
| |
| public static boolean isAutoGeneratedSchema(XmlFile file) { |
| return file.getUserData(AUTO_GENERATED) != null; |
| } |
| |
| private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.xml.XmlDocumentImpl"); |
| private volatile XmlProlog myProlog; |
| private volatile XmlTag myRootTag; |
| private volatile long myExtResourcesModCount = -1; |
| |
| public XmlDocumentImpl() { |
| this(XmlElementType.XML_DOCUMENT); |
| } |
| |
| protected XmlDocumentImpl(IElementType type) { |
| super(type); |
| } |
| |
| @Override |
| public void accept(@NotNull PsiElementVisitor visitor) { |
| if (visitor instanceof XmlElementVisitor) { |
| ((XmlElementVisitor)visitor).visitXmlDocument(this); |
| } |
| else { |
| visitor.visitElement(this); |
| } |
| } |
| |
| @Override |
| public int getChildRole(ASTNode child) { |
| LOG.assertTrue(child.getTreeParent() == this); |
| IElementType i = child.getElementType(); |
| if (i == XmlElementType.XML_PROLOG) { |
| return XmlChildRole.XML_PROLOG; |
| } |
| else if (i == XmlElementType.XML_TAG) { |
| return XmlChildRole.XML_TAG; |
| } |
| else { |
| return ChildRoleBase.NONE; |
| } |
| } |
| |
| @Override |
| public XmlProlog getProlog() { |
| XmlProlog prolog = myProlog; |
| |
| if (prolog == null) { |
| synchronized (PsiLock.LOCK) { |
| prolog = myProlog; |
| if (prolog == null) { |
| prolog = (XmlProlog)findElementByTokenType(XmlElementType.XML_PROLOG); |
| myProlog = prolog; |
| } |
| } |
| } |
| |
| return myProlog; |
| } |
| |
| @Override |
| public XmlTag getRootTag() { |
| XmlTag rootTag = myRootTag; |
| |
| if (rootTag == null) { |
| synchronized (PsiLock.LOCK) { |
| rootTag = myRootTag; |
| if (rootTag == null) { |
| rootTag = (XmlTag)findElementByTokenType(XmlElementType.XML_TAG); |
| myRootTag = rootTag; |
| } |
| } |
| } |
| |
| return myRootTag; |
| } |
| |
| @Override |
| @SuppressWarnings("ConstantConditions") |
| public XmlNSDescriptor getRootTagNSDescriptor() { |
| XmlTag rootTag = getRootTag(); |
| return rootTag != null ? rootTag.getNSDescriptor(rootTag.getNamespace(), false) : null; |
| } |
| |
| private ConcurrentHashMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheStrict = new ConcurrentHashMap<String, CachedValue<XmlNSDescriptor>>(); |
| private ConcurrentHashMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheNotStrict = new ConcurrentHashMap<String, CachedValue<XmlNSDescriptor>>(); |
| |
| @Override |
| public void clearCaches() { |
| myDefaultDescriptorsCacheStrict.clear(); |
| myDefaultDescriptorsCacheNotStrict.clear(); |
| myProlog = null; |
| myRootTag = null; |
| super.clearCaches(); |
| } |
| |
| @Override |
| public XmlNSDescriptor getDefaultNSDescriptor(final String namespace, final boolean strict) { |
| long curExtResourcesModCount = ExternalResourceManagerEx.getInstanceEx().getModificationCount(getProject()); |
| if (myExtResourcesModCount != curExtResourcesModCount) { |
| myDefaultDescriptorsCacheNotStrict.clear(); |
| myDefaultDescriptorsCacheStrict.clear(); |
| myExtResourcesModCount = curExtResourcesModCount; |
| } |
| |
| final ConcurrentHashMap<String, CachedValue<XmlNSDescriptor>> defaultDescriptorsCache; |
| if (strict) { |
| defaultDescriptorsCache = myDefaultDescriptorsCacheStrict; |
| } |
| else { |
| defaultDescriptorsCache = myDefaultDescriptorsCacheNotStrict; |
| } |
| |
| CachedValue<XmlNSDescriptor> cachedValue = defaultDescriptorsCache.get(namespace); |
| if (cachedValue == null) { |
| defaultDescriptorsCache.put(namespace, cachedValue = new PsiCachedValueImpl<XmlNSDescriptor>(getManager(), new CachedValueProvider<XmlNSDescriptor>() { |
| @Override |
| public Result<XmlNSDescriptor> compute() { |
| final XmlNSDescriptor defaultNSDescriptorInner = getDefaultNSDescriptorInner(namespace, strict); |
| |
| if (isGeneratedFromDtd(defaultNSDescriptorInner)) { |
| return new Result<XmlNSDescriptor>(defaultNSDescriptorInner, XmlDocumentImpl.this, ExternalResourceManager.getInstance()); |
| } |
| |
| return new Result<XmlNSDescriptor>(defaultNSDescriptorInner, defaultNSDescriptorInner != null |
| ? defaultNSDescriptorInner.getDependences() |
| : ExternalResourceManager.getInstance()); |
| } |
| })); |
| } |
| return cachedValue.getValue(); |
| } |
| |
| private boolean isGeneratedFromDtd(XmlNSDescriptor defaultNSDescriptorInner) { |
| if (defaultNSDescriptorInner == null) { |
| return false; |
| } |
| XmlFile descriptorFile = defaultNSDescriptorInner.getDescriptorFile(); |
| if (descriptorFile == null) { |
| return false; |
| } |
| @NonNls String otherName = XmlUtil.getContainingFile(this).getName() + ".dtd"; |
| return descriptorFile.getName().equals(otherName); |
| } |
| |
| private XmlNSDescriptor getDefaultNSDescriptorInner(final String namespace, final boolean strict) { |
| final XmlFile containingFile = XmlUtil.getContainingFile(this); |
| if (containingFile == null) return null; |
| final XmlProlog prolog = getProlog(); |
| final XmlDoctype doctype = prolog != null ? prolog.getDoctype() : null; |
| boolean dtdUriFromDocTypeIsNamespace = false; |
| |
| if (XmlUtil.HTML_URI.equals(namespace)) { |
| XmlNSDescriptor nsDescriptor = doctype != null ? getNsDescriptorFormDocType(doctype, containingFile, true) : null; |
| if (doctype != null) { |
| LOG.debug( |
| "Descriptor from doctype " + doctype + " is " + (nsDescriptor != null ? nsDescriptor.getClass().getCanonicalName() : "NULL")); |
| } |
| |
| if (nsDescriptor == null) { |
| String htmlns = ExternalResourceManagerEx.getInstanceEx().getDefaultHtmlDoctype(getProject()); |
| if (htmlns.isEmpty()) { |
| htmlns = Html5SchemaProvider.getHtml5SchemaLocation(); |
| } |
| nsDescriptor = getDefaultNSDescriptor(htmlns, false); |
| } |
| return new HtmlNSDescriptorImpl(nsDescriptor); |
| } |
| else if (XmlUtil.XHTML_URI.equals(namespace)) { |
| String xhtmlNamespace = XmlUtil.getDefaultXhtmlNamespace(getProject()); |
| if (xhtmlNamespace == null || xhtmlNamespace.isEmpty()) { |
| xhtmlNamespace = Html5SchemaProvider.getXhtml5SchemaLocation(); |
| } |
| return getDefaultNSDescriptor(xhtmlNamespace, false); |
| } |
| else if (namespace != null && namespace != XmlUtil.EMPTY_URI) { |
| if (doctype == null || !namespace.equals(XmlUtil.getDtdUri(doctype))) { |
| boolean documentIsSchemaThatDefinesNs = namespace.equals(XmlUtil.getTargetSchemaNsFromTag(getRootTag())); |
| |
| final XmlFile xmlFile = documentIsSchemaThatDefinesNs |
| ? containingFile |
| : XmlUtil.findNamespace(containingFile, namespace); |
| if (xmlFile != null) { |
| final XmlDocument document = xmlFile.getDocument(); |
| if (document != null) { |
| return (XmlNSDescriptor)document.getMetaData(); |
| } |
| } |
| } else { |
| dtdUriFromDocTypeIsNamespace = true; |
| } |
| } |
| |
| if (strict && !dtdUriFromDocTypeIsNamespace) return null; |
| |
| if (doctype != null) { |
| XmlNSDescriptor descr = getNsDescriptorFormDocType(doctype, containingFile, false); |
| |
| if (descr != null) { |
| return XmlExtension.getExtension(containingFile).getDescriptorFromDoctype(containingFile, descr); |
| } |
| } |
| |
| if (strict) return null; |
| if (namespace == XmlUtil.EMPTY_URI) { |
| final XmlFile xmlFile = XmlUtil.findNamespace(containingFile, namespace); |
| if (xmlFile != null) { |
| return (XmlNSDescriptor)xmlFile.getDocument().getMetaData(); |
| } |
| } |
| try { |
| final PsiFile fileFromText = PsiFileFactory.getInstance(getProject()) |
| .createFileFromText(containingFile.getName() + ".dtd", DTDLanguage.INSTANCE, XmlUtil.generateDocumentDTD(this, false), false, false); |
| if (fileFromText instanceof XmlFile) { |
| fileFromText.putUserData(AUTO_GENERATED, Boolean.TRUE); |
| return (XmlNSDescriptor)((XmlFile)fileFromText).getDocument().getMetaData(); |
| } |
| } |
| catch (ProcessCanceledException ex) { |
| throw ex; |
| } |
| catch (RuntimeException ignored) { |
| } // e.g. dtd isn't mapped to xml type |
| |
| return null; |
| } |
| |
| @NotNull |
| private static String getFilePathForLogging(@Nullable PsiFile file) { |
| if (file == null) { |
| return "NULL"; |
| } |
| final VirtualFile vFile = file.getVirtualFile(); |
| return vFile != null ? vFile.getPath() : "NULL_VFILE"; |
| } |
| |
| @Nullable |
| private XmlNSDescriptor getNsDescriptorFormDocType(final XmlDoctype doctype, final XmlFile containingFile, final boolean forHtml) { |
| XmlNSDescriptor descriptor = getNSDescriptorFromMetaData(doctype.getMarkupDecl(), true); |
| |
| final String filePath = getFilePathForLogging(containingFile); |
| |
| final String dtdUri = XmlUtil.getDtdUri(doctype); |
| LOG.debug("DTD url for doctype " + doctype.getText() + " in file " + filePath + " is " + dtdUri); |
| |
| if (dtdUri != null && !dtdUri.isEmpty()){ |
| XmlFile xmlFile = XmlUtil.findNamespace(containingFile, dtdUri); |
| if (xmlFile == null) { |
| // try to auto-detect it |
| xmlFile = XmlNamespaceIndex.guessDtd(dtdUri, containingFile); |
| } |
| final String schemaFilePath = getFilePathForLogging(xmlFile); |
| |
| LOG.debug("Schema file for " + filePath + " is " + schemaFilePath); |
| |
| XmlNSDescriptor descriptorFromDtd = getNSDescriptorFromMetaData(xmlFile == null ? null : xmlFile.getDocument(), forHtml); |
| |
| LOG.debug("Descriptor from meta data for schema file " + |
| schemaFilePath + |
| " is " + |
| (descriptorFromDtd != null ? descriptorFromDtd.getClass().getCanonicalName() : "NULL")); |
| |
| if (descriptor != null && descriptorFromDtd != null){ |
| descriptor = new XmlNSDescriptorSequence(new XmlNSDescriptor[]{descriptor, descriptorFromDtd}); |
| } |
| else if (descriptorFromDtd != null) { |
| descriptor = descriptorFromDtd; |
| } |
| } |
| return descriptor; |
| } |
| |
| @Nullable |
| private XmlNSDescriptor getNSDescriptorFromMetaData(@Nullable PsiMetaOwner metaOwner, boolean nonEmpty) { |
| if (metaOwner == null) return null; |
| XmlNSDescriptor descriptor = (XmlNSDescriptor)metaOwner.getMetaData(); |
| if (descriptor == null) return null; |
| if (nonEmpty && descriptor.getRootElementsDescriptors(this).length == 0) { |
| return null; |
| } |
| return descriptor; |
| } |
| |
| @Override |
| public CompositePsiElement clone() { |
| HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>( |
| myDefaultDescriptorsCacheStrict |
| ); |
| HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>( |
| myDefaultDescriptorsCacheNotStrict |
| ); |
| final XmlDocumentImpl copy = (XmlDocumentImpl) super.clone(); |
| updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict); |
| return copy; |
| } |
| |
| @Override |
| public PsiElement copy() { |
| HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>( |
| myDefaultDescriptorsCacheStrict |
| ); |
| HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict = new HashMap<String, CachedValue<XmlNSDescriptor>>( |
| myDefaultDescriptorsCacheNotStrict |
| ); |
| final XmlDocumentImpl copy = (XmlDocumentImpl)super.copy(); |
| updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict); |
| return copy; |
| } |
| |
| private void updateSelfDependentDtdDescriptors(XmlDocumentImpl copy, HashMap<String, |
| CachedValue<XmlNSDescriptor>> cacheStrict, HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict) { |
| copy.myDefaultDescriptorsCacheNotStrict = new ConcurrentHashMap<String, CachedValue<XmlNSDescriptor>>(); |
| copy.myDefaultDescriptorsCacheStrict = new ConcurrentHashMap<String, CachedValue<XmlNSDescriptor>>(); |
| |
| for(Map.Entry<String, CachedValue<XmlNSDescriptor>> e:cacheStrict.entrySet()) { |
| if (e.getValue().hasUpToDateValue()) { |
| final XmlNSDescriptor nsDescriptor = e.getValue().getValue(); |
| if (!isGeneratedFromDtd(nsDescriptor)) copy.myDefaultDescriptorsCacheStrict.put(e.getKey(), e.getValue()); |
| } |
| } |
| |
| for(Map.Entry<String, CachedValue<XmlNSDescriptor>> e:cacheNotStrict.entrySet()) { |
| if (e.getValue().hasUpToDateValue()) { |
| final XmlNSDescriptor nsDescriptor = e.getValue().getValue(); |
| if (!isGeneratedFromDtd(nsDescriptor)) copy.myDefaultDescriptorsCacheNotStrict.put(e.getKey(), e.getValue()); |
| } |
| } |
| } |
| |
| @Override |
| public PsiMetaData getMetaData() { |
| return MetaRegistry.getMeta(this); |
| } |
| |
| @SuppressWarnings({"HardCodedStringLiteral"}) |
| public void dumpStatistics(){ |
| System.out.println("Statistics:"); |
| final TObjectIntHashMap<Object> map = new TObjectIntHashMap<Object>(); |
| |
| final PsiElementVisitor psiRecursiveElementVisitor = new XmlRecursiveElementVisitor(){ |
| @NonNls private static final String TOKENS_KEY = "Tokens"; |
| @NonNls private static final String ELEMENTS_KEY = "Elements"; |
| |
| @Override public void visitXmlToken(XmlToken token) { |
| inc(TOKENS_KEY); |
| } |
| |
| @Override public void visitElement(PsiElement element) { |
| inc(ELEMENTS_KEY); |
| super.visitElement(element); |
| } |
| |
| private void inc(final String key) { |
| map.put(key, map.get(key) + 1); |
| } |
| }; |
| |
| accept(psiRecursiveElementVisitor); |
| |
| final Object[] keys = map.keys(); |
| for (final Object key : keys) { |
| System.out.println(key + ": " + map.get(key)); |
| } |
| } |
| |
| @Override |
| public TreeElement addInternal(final TreeElement first, final ASTNode last, final ASTNode anchor, final Boolean before) { |
| final PomModel model = PomManager.getModel(getProject()); |
| final XmlAspect aspect = model.getModelAspect(XmlAspect.class); |
| final TreeElement[] holder = new TreeElement[1]; |
| try{ |
| model.runTransaction(new PomTransactionBase(this, aspect) { |
| @Override |
| public PomModelEvent runInner() { |
| holder[0] = XmlDocumentImpl.super.addInternal(first, last, anchor, before); |
| return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this); |
| } |
| }); |
| } |
| catch(IncorrectOperationException ignored){} |
| return holder[0]; |
| } |
| |
| @Override |
| public void deleteChildInternal(@NotNull final ASTNode child) { |
| final PomModel model = PomManager.getModel(getProject()); |
| final XmlAspect aspect = model.getModelAspect(XmlAspect.class); |
| try{ |
| model.runTransaction(new PomTransactionBase(this, aspect) { |
| @Override |
| public PomModelEvent runInner() { |
| XmlDocumentImpl.super.deleteChildInternal(child); |
| return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this); |
| } |
| }); |
| } |
| catch(IncorrectOperationException ignored){} |
| } |
| |
| @Override |
| public void replaceChildInternal(@NotNull final ASTNode child, @NotNull final TreeElement newElement) { |
| final PomModel model = PomManager.getModel(getProject()); |
| final XmlAspect aspect = model.getModelAspect(XmlAspect.class); |
| try{ |
| model.runTransaction(new PomTransactionBase(this, aspect) { |
| @Override |
| public PomModelEvent runInner() { |
| XmlDocumentImpl.super.replaceChildInternal(child, newElement); |
| return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this); |
| } |
| }); |
| } |
| catch(IncorrectOperationException ignored){} |
| } |
| } |