| /* |
| * 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); |
| } |
| } |