blob: 758dea80e9dbf6b3de33978488c114276ff5afb3 [file] [log] [blame]
/*
* Copyright 2007 Sascha Weinreuter
*
* 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 org.intellij.plugins.relaxNG.validation;
import com.intellij.lang.ASTNode;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.ExternalAnnotator;
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.fileTypes.StdFileTypes;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.*;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import org.intellij.plugins.relaxNG.ApplicationLoader;
import org.intellij.plugins.relaxNG.compact.RncFileType;
import org.intellij.plugins.relaxNG.compact.psi.RncFile;
import org.intellij.plugins.relaxNG.model.resolve.RelaxIncludeIndex;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* Created by IntelliJ IDEA.
* User: sweinreuter
* Date: 18.07.2007
*/
public class RngSchemaValidator extends ExternalAnnotator<RngSchemaValidator.MyValidationMessageConsumer,RngSchemaValidator.MyValidationMessageConsumer> {
private static final Logger LOG = Logger.getInstance(RngSchemaValidator.class.getName());
@Nullable
@Override
public MyValidationMessageConsumer collectInformation(@NotNull final PsiFile file) {
final FileType type = file.getFileType();
if (type != StdFileTypes.XML && type != RncFileType.getInstance()) {
return null;
}
final XmlFile xmlfile = (XmlFile)file;
final XmlDocument document = xmlfile.getDocument();
if (document == null) {
return null;
}
if (type == StdFileTypes.XML) {
final XmlTag rootTag = document.getRootTag();
if (rootTag == null) {
return null;
}
if (!ApplicationLoader.RNG_NAMESPACE.equals(rootTag.getNamespace())) {
return null;
}
} else {
if (!ApplicationManager.getApplication().isUnitTestMode() && MyErrorFinder.hasError(xmlfile)) {
return null;
}
}
final Document doc = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
final MyValidationMessageConsumer consumer = new MyValidationMessageConsumer();
ErrorHandler eh = new DefaultHandler() {
@Override
public void warning(SAXParseException e) {
handleError(e, file, doc, consumer.warning());
}
@Override
public void error(SAXParseException e) {
handleError(e, file, doc, consumer.error());
}
};
RngParser.parsePattern(file, eh, true);
return consumer;
}
@Nullable
@Override
public MyValidationMessageConsumer doAnnotate(MyValidationMessageConsumer collectedInfo) {
return collectedInfo;
}
@Override
public void apply(@NotNull PsiFile file,
MyValidationMessageConsumer annotationResult,
@NotNull AnnotationHolder holder) {
annotationResult.apply(holder);
}
static class MyValidationMessageConsumer {
final List<Pair<PsiElement, String >> errors = new ArrayList<Pair<PsiElement, String>>();
final List<Pair<PsiElement, String >> warnings = new ArrayList<Pair<PsiElement, String>>();
ValidationMessageConsumer error() {
return new ValidationMessageConsumer() {
@Override
public void onMessage(PsiElement context, String message) {
errors.add(Pair.create(context, message));
}
};
}
ValidationMessageConsumer warning() {
return new ValidationMessageConsumer() {
@Override
public void onMessage(PsiElement context, String message) {
warnings.add(Pair.create(context, message));
}
};
}
void apply(AnnotationHolder holder) {
MessageConsumerImpl errorc = new ErrorMessageConsumer(holder);
MessageConsumerImpl warningc = new WarningMessageConsumer(holder);
for (Pair<PsiElement, String> error : errors) {
errorc.onMessage(error.first, error.second);
}
for (Pair<PsiElement, String> warning : warnings) {
warningc.onMessage(warning.first, warning.second);
}
}
}
public static void handleError(SAXParseException ex, PsiFile file, Document document, ValidationMessageConsumer consumer) {
final String systemId = ex.getSystemId();
if (LOG.isDebugEnabled()) {
LOG.debug("RNG Schema error: " + ex.getMessage() + " [" + systemId + "]");
}
if (systemId != null) {
final VirtualFile virtualFile = findVirtualFile(systemId);
if (!Comparing.equal(virtualFile, file.getVirtualFile())) {
return;
}
}
final PsiElement at;
final int line = ex.getLineNumber();
if (line > 0) {
final int column = ex.getColumnNumber();
final int startOffset = document.getLineStartOffset(line - 1);
if (column > 0) {
if (file.getFileType() == RncFileType.getInstance()) {
final PsiElement e = file.findElementAt(startOffset + column);
if (e == null) {
at = e;
} else {
at = file.findElementAt(startOffset + column - 1);
}
} else {
at = file.findElementAt(startOffset + column - 2);
}
} else {
final PsiElement e = file.findElementAt(startOffset);
at = e != null ? PsiTreeUtil.nextLeaf(e) : null;
}
} else {
final XmlDocument d = ((XmlFile)file).getDocument();
assert d != null;
final XmlTag rootTag = d.getRootTag();
assert rootTag != null;
at = rootTag.getFirstChild();
}
final PsiElement host;
if (file instanceof RncFile) {
host = at;
} else {
host = PsiTreeUtil.getParentOfType(at, XmlAttribute.class, XmlTag.class);
}
if (at != null && host != null) {
consumer.onMessage(host, ex.getMessage());
} else {
consumer.onMessage(file, ex.getMessage());
}
}
public static VirtualFile findVirtualFile(String systemId) {
try {
return VfsUtil.findFileByURL(new URL(systemId));
} catch (Exception e) {
LOG.warn("Failed to build file from uri <" + systemId + ">", e);
return VirtualFileManager.getInstance().findFileByUrl(VfsUtilCore.fixURLforIDEA(systemId));
}
}
public interface ValidationMessageConsumer {
void onMessage(PsiElement context, String message);
}
private abstract static class MessageConsumerImpl implements ValidationMessageConsumer {
protected final AnnotationHolder myHolder;
public MessageConsumerImpl(AnnotationHolder holder) {
myHolder = holder;
}
@Override
public void onMessage(PsiElement host, String message) {
final ASTNode node = host.getNode();
assert node != null;
if (host instanceof XmlAttribute) {
final ASTNode nameNode = XmlChildRole.ATTRIBUTE_NAME_FINDER.findChild(node);
createAnnotation(nameNode, message);
} else if (host instanceof XmlTag) {
final ASTNode start = XmlChildRole.START_TAG_NAME_FINDER.findChild(node);
if (start != null) {
createAnnotation(start, message);
}
final ASTNode end = XmlChildRole.CLOSING_TAG_NAME_FINDER.findChild(node);
if (end != null) {
createAnnotation(end, message);
}
} else {
createAnnotation(node, message);
}
}
protected abstract void createAnnotation(ASTNode node, String message);
}
private static class ErrorMessageConsumer extends MessageConsumerImpl {
@NonNls
private static final String MISSING_START_ELEMENT = "missing \"start\" element";
private static final String UNDEFINED_PATTERN = "reference to undefined pattern ";
public ErrorMessageConsumer(AnnotationHolder holder) {
super(holder);
}
@Override
protected void createAnnotation(ASTNode node, String message) {
if (MISSING_START_ELEMENT.equals(message)) {
final PsiFile psiFile = node.getPsi().getContainingFile();
if (psiFile instanceof XmlFile) {
final PsiElementProcessor.FindElement<XmlFile> processor = new PsiElementProcessor.FindElement<XmlFile>();
RelaxIncludeIndex.processBackwardDependencies((XmlFile)psiFile, processor);
if (processor.isFound()) {
// files that are included from other files do not need a <start> element.
myHolder.createWeakWarningAnnotation(node, message);
return;
}
}
} else if (message != null && message.startsWith(UNDEFINED_PATTERN)) {
// we've got our own validation for that
return;
}
myHolder.createErrorAnnotation(node, message);
}
}
private static class WarningMessageConsumer extends MessageConsumerImpl {
public WarningMessageConsumer(AnnotationHolder holder) {
super(holder);
}
@Override
protected void createAnnotation(ASTNode node, String message) {
myHolder.createWarningAnnotation(node, message);
}
}
private static class MyErrorFinder extends PsiRecursiveElementVisitor {
private static final MyErrorFinder INSTANCE = new MyErrorFinder();
private static final class HasError extends RuntimeException {
}
private static final HasError FOUND = new HasError();
@Override
public void visitErrorElement(PsiErrorElement element) {
throw FOUND;
}
public static boolean hasError(PsiElement element) {
try {
element.accept(INSTANCE);
return false;
} catch (HasError e) {
return true;
}
}
}
}