blob: debb58ae089152beb446b1a6520f435bacbded33 [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.xml.impl;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.daemon.Validator;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.ex.InspectionToolWrapper;
import com.intellij.ide.highlighter.XHtmlFileType;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.lang.Language;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.reference.SoftReference;
import com.intellij.xml.actions.validate.ErrorReporter;
import com.intellij.xml.actions.validate.ValidateXmlActionHandler;
import com.intellij.xml.util.XmlResourceResolver;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.xml.sax.SAXParseException;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.List;
/**
* @author maxim
*/
public class ExternalDocumentValidator {
private static final Logger LOG = Logger.getInstance("#com.intellij.xml.impl.ExternalDocumentValidator");
private static final Key<SoftReference<ExternalDocumentValidator>> validatorInstanceKey = Key.create("validatorInstance");
public static final @NonNls String INSPECTION_SHORT_NAME = "CheckXmlFileWithXercesValidator";
private ValidateXmlActionHandler myHandler;
private Validator.ValidationHost myHost;
private long myModificationStamp;
private PsiFile myFile;
@NonNls
private static final String CANNOT_FIND_DECLARATION_ERROR_PREFIX = "Cannot find the declaration of element";
@NonNls
private static final String ELEMENT_ERROR_PREFIX = "Element";
@NonNls
private static final String ROOT_ELEMENT_ERROR_PREFIX = "Document root element";
@NonNls
private static final String CONTENT_OF_ELEMENT_TYPE_ERROR_PREFIX = "The content of element type";
@NonNls
private static final String VALUE_ERROR_PREFIX = "Value ";
@NonNls
private static final String ATTRIBUTE_ERROR_PREFIX = "Attribute ";
@NonNls
private static final String STRING_ERROR_PREFIX = "The string";
@NonNls
private static final String ATTRIBUTE_MESSAGE_PREFIX = "cvc-attribute.";
private static class ValidationInfo {
final PsiElement element;
final String message;
final Validator.ValidationHost.ErrorType type;
private ValidationInfo(PsiElement element, String message, Validator.ValidationHost.ErrorType type) {
this.element = element;
this.message = message;
this.type = type;
}
}
private WeakReference<List<ValidationInfo>> myInfos; // last jaxp validation result
private void runJaxpValidation(final XmlElement element, Validator.ValidationHost host) {
final PsiFile file = element.getContainingFile();
if (myFile == file &&
file != null &&
myModificationStamp == file.getModificationStamp() &&
!ValidateXmlActionHandler.isValidationDependentFilesOutOfDate((XmlFile)file) &&
SoftReference.dereference(myInfos)!=null // we have validated before
) {
addAllInfos(host,myInfos.get());
return;
}
if (myHandler==null) myHandler = new ValidateXmlActionHandler(false);
final Project project = element.getProject();
final Document document = PsiDocumentManager.getInstance(project).getDocument(file);
if (document==null) return;
final List<ValidationInfo> results = new LinkedList<ValidationInfo>();
myHost = new Validator.ValidationHost() {
@Override
public void addMessage(PsiElement context, String message, int type) {
addMessage(context, message, type==ERROR?ErrorType.ERROR : type==WARNING?ErrorType.WARNING : ErrorType.INFO);
}
@Override
public void addMessage(final PsiElement context, final String message, @NotNull final ErrorType type) {
final ValidationInfo o = new ValidationInfo(context, message, type);
results.add(o);
}
};
myHandler.setErrorReporter(new ErrorReporter(myHandler) {
@Override
public boolean isStopOnUndeclaredResource() {
return true;
}
@Override
public void processError(final SAXParseException e, final ValidateXmlActionHandler.ProblemType warning) {
try {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
if (e.getPublicId() != null) {
return;
}
final VirtualFile errorFile = myHandler.getFile(e.getPublicId(), e.getSystemId());
if (!Comparing.equal(errorFile, file.getVirtualFile()) && errorFile != null) {
return; // error in attached schema
}
if (document.getLineCount() < e.getLineNumber() || e.getLineNumber() <= 0) {
return;
}
Validator.ValidationHost.ErrorType problemType = getProblemType(warning);
int offset = Math.max(0, document.getLineStartOffset(e.getLineNumber() - 1) + e.getColumnNumber() - 2);
if (offset >= document.getTextLength()) return;
PsiElement currentElement = PsiDocumentManager.getInstance(project).getPsiFile(document).findElementAt(offset);
PsiElement originalElement = currentElement;
final String elementText = currentElement.getText();
if (elementText.equals("</")) {
currentElement = currentElement.getNextSibling();
}
else if (elementText.equals(">") || elementText.equals("=")) {
currentElement = currentElement.getPrevSibling();
}
// Cannot find the declaration of element
String localizedMessage = e.getLocalizedMessage();
// Ideally would be to switch one messageIds
int endIndex = localizedMessage.indexOf(':');
if (endIndex < localizedMessage.length() - 1 && localizedMessage.charAt(endIndex + 1) == '/') {
endIndex = -1; // ignore : in http://
}
String messageId = endIndex != -1 ? localizedMessage.substring(0, endIndex ):"";
localizedMessage = localizedMessage.substring(endIndex + 1).trim();
if (localizedMessage.startsWith(CANNOT_FIND_DECLARATION_ERROR_PREFIX) ||
localizedMessage.startsWith(ELEMENT_ERROR_PREFIX) ||
localizedMessage.startsWith(ROOT_ELEMENT_ERROR_PREFIX) ||
localizedMessage.startsWith(CONTENT_OF_ELEMENT_TYPE_ERROR_PREFIX)
) {
addProblemToTagName(currentElement, originalElement, localizedMessage, warning);
//return;
} else if (localizedMessage.startsWith(VALUE_ERROR_PREFIX)) {
addProblemToTagName(currentElement, originalElement, localizedMessage, warning);
} else {
if (messageId.startsWith(ATTRIBUTE_MESSAGE_PREFIX)) {
@NonNls String prefix = "of attribute ";
final int i = localizedMessage.indexOf(prefix);
if (i != -1) {
int messagePrefixLength = prefix.length() + i;
final int nextQuoteIndex = localizedMessage.indexOf(localizedMessage.charAt(messagePrefixLength), messagePrefixLength + 1);
String attrName = nextQuoteIndex == -1 ? null : localizedMessage.substring(messagePrefixLength + 1, nextQuoteIndex);
XmlTag parent = PsiTreeUtil.getParentOfType(originalElement,XmlTag.class);
currentElement = parent.getAttribute(attrName,null);
if (currentElement != null) {
currentElement = ((XmlAttribute)currentElement).getValueElement();
}
}
if (currentElement!=null) {
assertValidElement(currentElement, originalElement,localizedMessage);
myHost.addMessage(currentElement,localizedMessage, problemType);
} else {
addProblemToTagName(originalElement, originalElement, localizedMessage, warning);
}
}
else if (localizedMessage.startsWith(ATTRIBUTE_ERROR_PREFIX)) {
final int messagePrefixLength = ATTRIBUTE_ERROR_PREFIX.length();
if ( localizedMessage.charAt(messagePrefixLength) == '"' ||
localizedMessage.charAt(messagePrefixLength) == '\''
) {
// extract the attribute name from message and get it from tag!
final int nextQuoteIndex = localizedMessage.indexOf(localizedMessage.charAt(messagePrefixLength), messagePrefixLength + 1);
String attrName = nextQuoteIndex == -1 ? null : localizedMessage.substring(messagePrefixLength + 1, nextQuoteIndex);
XmlTag parent = PsiTreeUtil.getParentOfType(originalElement,XmlTag.class);
currentElement = parent.getAttribute(attrName,null);
if (currentElement!=null) {
currentElement = SourceTreeToPsiMap.treeElementToPsi(
XmlChildRole.ATTRIBUTE_NAME_FINDER.findChild(
SourceTreeToPsiMap.psiElementToTree(currentElement)
)
);
}
} else {
currentElement = PsiTreeUtil.getParentOfType(currentElement, XmlTag.class, false);
}
if (currentElement!=null) {
assertValidElement(currentElement, originalElement,localizedMessage);
myHost.addMessage(currentElement,localizedMessage, problemType);
} else {
addProblemToTagName(originalElement, originalElement, localizedMessage, warning);
}
} else if (localizedMessage.startsWith(STRING_ERROR_PREFIX)) {
if (currentElement != null) {
myHost.addMessage(currentElement,localizedMessage, Validator.ValidationHost.ErrorType.WARNING);
}
}
else {
currentElement = getNodeForMessage(currentElement != null ? currentElement:originalElement);
assertValidElement(currentElement, originalElement,localizedMessage);
if (currentElement!=null) {
myHost.addMessage(currentElement,localizedMessage, problemType);
}
}
}
}
});
}
catch (Exception ex) {
if (ex instanceof ProcessCanceledException) throw (ProcessCanceledException)ex;
if (ex instanceof XmlResourceResolver.IgnoredResourceException) throw (XmlResourceResolver.IgnoredResourceException)ex;
LOG.error(ex);
}
}
});
myHandler.doValidate((XmlFile)element.getContainingFile());
myFile = file;
myModificationStamp = myFile.getModificationStamp();
myInfos = new WeakReference<List<ValidationInfo>>(results);
addAllInfos(host,results);
}
private static Validator.ValidationHost.ErrorType getProblemType(ValidateXmlActionHandler.ProblemType warning) {
return warning == ValidateXmlActionHandler.ProblemType.WARNING ? Validator.ValidationHost.ErrorType.WARNING : Validator.ValidationHost.ErrorType.ERROR;
}
private static PsiElement getNodeForMessage(final PsiElement currentElement) {
PsiElement parentOfType = PsiTreeUtil.getNonStrictParentOfType(
currentElement,
XmlTag.class,
XmlProcessingInstruction.class,
XmlElementDecl.class,
XmlMarkupDecl.class,
XmlEntityRef.class,
XmlDoctype.class
);
if (parentOfType == null) {
if (currentElement instanceof XmlToken) {
parentOfType = currentElement.getParent();
}
else {
parentOfType = currentElement;
}
}
return parentOfType;
}
private static void addAllInfos(Validator.ValidationHost host,List<ValidationInfo> highlightInfos) {
for (ValidationInfo info : highlightInfos) {
host.addMessage(info.element, info.message, info.type);
}
}
private PsiElement addProblemToTagName(PsiElement currentElement,
final PsiElement originalElement,
final String localizedMessage,
final ValidateXmlActionHandler.ProblemType problemType) {
currentElement = PsiTreeUtil.getParentOfType(currentElement,XmlTag.class,false);
if (currentElement==null) {
currentElement = PsiTreeUtil.getParentOfType(originalElement,XmlElementDecl.class,false);
}
if (currentElement == null) {
currentElement = originalElement;
}
assertValidElement(currentElement, originalElement,localizedMessage);
if (currentElement!=null) {
myHost.addMessage(currentElement,localizedMessage, getProblemType(problemType));
}
return currentElement;
}
private static void assertValidElement(PsiElement currentElement, PsiElement originalElement, String message) {
if (currentElement==null) {
XmlTag tag = PsiTreeUtil.getParentOfType(originalElement, XmlTag.class);
LOG.error("The validator message:" + message + " is bound to null node,\n" + "initial element:" + originalElement.getText() + ",\n" +
"parent:" + originalElement.getParent() + ",\n" + "tag:" + (tag != null ? tag.getText() : "null") + ",\n" +
"offset in tag: " + (originalElement.getTextOffset() - (tag == null ? 0 : tag.getTextOffset())));
}
}
public static synchronized void doValidation(final XmlDocument document, final Validator.ValidationHost host) {
final PsiFile containingFile = document.getContainingFile();
if (containingFile == null) {
return;
}
if (containingFile.getViewProvider() instanceof TemplateLanguageFileViewProvider) {
return;
}
final FileType fileType = containingFile.getViewProvider().getVirtualFile().getFileType();
if (fileType != XmlFileType.INSTANCE && fileType != XHtmlFileType.INSTANCE) {
return;
}
for(Language lang: containingFile.getViewProvider().getLanguages()) {
if ("ANT".equals(lang.getID())) return;
}
final XmlTag rootTag = document.getRootTag();
if (rootTag == null) return;
String namespace = rootTag.getNamespace();
if (XmlUtil.ANT_URI.equals(namespace)) return;
final Project project = document.getProject();
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
final InspectionToolWrapper toolWrapper =
profile.getInspectionTool(INSPECTION_SHORT_NAME, containingFile);
if (toolWrapper == null) return;
if (!profile.isToolEnabled(HighlightDisplayKey.find(INSPECTION_SHORT_NAME), containingFile)) return;
SoftReference<ExternalDocumentValidator> validatorReference = project.getUserData(validatorInstanceKey);
ExternalDocumentValidator validator = SoftReference.dereference(validatorReference);
if(validator == null) {
validator = new ExternalDocumentValidator();
project.putUserData(validatorInstanceKey,new SoftReference<ExternalDocumentValidator>(validator));
}
validator.runJaxpValidation(document,host);
}
}