All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intellij.psi.impl.source.xml.XmlDocumentImpl Maven / Gradle / Ivy

Go to download

A packaging of the IntelliJ Community Edition xml-psi-impl library. This is release number 1 of trunk branch 142.

The newest version!
/*
 * Copyright 2000-2015 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.util.CachedValuesManager;
import com.intellij.psi.xml.*;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
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;
import java.util.concurrent.ConcurrentMap;

/**
 * @author Mike
 */
public class XmlDocumentImpl extends XmlElementImpl implements XmlDocument {

  private static final Key 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) {
      prolog = (XmlProlog)findElementByTokenType(XmlElementType.XML_PROLOG);
      synchronized (PsiLock.LOCK) {
        if (myProlog == null) {
          myProlog = prolog;
        } else {
          prolog = myProlog;
        }
      }
    }

    return prolog;
  }

  @Override
  public XmlTag getRootTag() {
    XmlTag rootTag = myRootTag;

    if (rootTag == null) {
      rootTag = (XmlTag)findElementByTokenType(XmlElementType.XML_TAG);
      synchronized (PsiLock.LOCK) {
        if (myRootTag == null) {
          myRootTag = rootTag;
        } else {
          rootTag = myRootTag;
        }
      }
    }

    return rootTag;
  }

  @Override
  @SuppressWarnings("ConstantConditions")
  public XmlNSDescriptor getRootTagNSDescriptor() {
    XmlTag rootTag = getRootTag();
    return rootTag != null ? rootTag.getNSDescriptor(rootTag.getNamespace(), false) : null;
  }

  private ConcurrentMap> myDefaultDescriptorsCacheStrict =
    ContainerUtil.newConcurrentMap();
  private ConcurrentMap> myDefaultDescriptorsCacheNotStrict =
    ContainerUtil.newConcurrentMap();

  @Override
  public void clearCaches() {
    myDefaultDescriptorsCacheStrict.clear();
    myDefaultDescriptorsCacheNotStrict.clear();
    synchronized (PsiLock.LOCK) {
      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 ConcurrentMap> defaultDescriptorsCache;
    if (strict) {
      defaultDescriptorsCache = myDefaultDescriptorsCacheStrict;
    }
    else {
      defaultDescriptorsCache = myDefaultDescriptorsCacheNotStrict;
    }

    CachedValue cachedValue = defaultDescriptorsCache.get(namespace);
    if (cachedValue == null) {
      defaultDescriptorsCache.put(namespace, cachedValue = new PsiCachedValueImpl(getManager(), new CachedValueProvider() {
        @Override
        public Result compute() {
          final XmlNSDescriptor defaultNSDescriptorInner = getDefaultNSDescriptorInner(namespace, strict);

          if (isGeneratedFromDtd(defaultNSDescriptorInner)) {
            return new Result(defaultNSDescriptorInner, XmlDocumentImpl.this, ExternalResourceManager.getInstance());
          }

          return new Result(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);
      }
      final XmlFile descriptorFile = nsDescriptor.getDescriptorFile();
      if (descriptorFile != null) {
        final XmlNSDescriptor finalNsDescriptor = nsDescriptor;
        return CachedValuesManager.getCachedValue(descriptorFile, new CachedValueProvider() {
          @Nullable
          @Override
          public Result compute() {
            return Result.create(new HtmlNSDescriptorImpl(finalNsDescriptor), descriptorFile);
          }
        });
      }
      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;
  }

  @NotNull
  @Override
  public CompositePsiElement clone() {
    HashMap> cacheStrict = new HashMap>(
      myDefaultDescriptorsCacheStrict
    );
    HashMap> cacheNotStrict = new HashMap>(
      myDefaultDescriptorsCacheNotStrict
    );
    final XmlDocumentImpl copy = (XmlDocumentImpl) super.clone();
    updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict);
    return copy;
  }

  @Override
  public PsiElement copy() {
    HashMap> cacheStrict = new HashMap>(
      myDefaultDescriptorsCacheStrict
    );
    HashMap> cacheNotStrict = new HashMap>(
      myDefaultDescriptorsCacheNotStrict
    );
    final XmlDocumentImpl copy = (XmlDocumentImpl)super.copy();
    updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict);
    return copy;
  }

  private void updateSelfDependentDtdDescriptors(XmlDocumentImpl copy, HashMap> cacheStrict, HashMap> cacheNotStrict) {
    copy.myDefaultDescriptorsCacheNotStrict = ContainerUtil.newConcurrentMap();
    copy.myDefaultDescriptorsCacheStrict = ContainerUtil.newConcurrentMap();

    for(Map.Entry> 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> 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 map = new TObjectIntHashMap();

    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){}
  }
}