update copyright
[fedora-idea.git] / xml / impl / src / com / intellij / xml / actions / ValidateXmlActionHandler.java
blobad5baade914bb4dfeafd7c5cd5a2e8f557f50c7c
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.xml.actions;
18 import com.intellij.codeInsight.CodeInsightActionHandler;
19 import com.intellij.ide.errorTreeView.NewErrorTreeViewPanel;
20 import com.intellij.javaee.UriUtil;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.command.CommandProcessor;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.editor.Editor;
25 import com.intellij.openapi.progress.ProcessCanceledException;
26 import com.intellij.openapi.project.Project;
27 import com.intellij.openapi.ui.Messages;
28 import com.intellij.openapi.util.Key;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.openapi.wm.ToolWindowId;
31 import com.intellij.openapi.wm.ToolWindowManager;
32 import com.intellij.openapi.wm.WindowManager;
33 import com.intellij.psi.PsiDocumentManager;
34 import com.intellij.psi.PsiFile;
35 import com.intellij.psi.PsiManager;
36 import com.intellij.psi.xml.*;
37 import com.intellij.ui.content.*;
38 import com.intellij.util.ui.ErrorTreeView;
39 import com.intellij.util.ui.MessageCategory;
40 import com.intellij.xml.XmlBundle;
41 import com.intellij.xml.util.XmlResourceResolver;
42 import org.apache.xerces.impl.Constants;
43 import org.apache.xerces.jaxp.JAXPConstants;
44 import org.apache.xerces.jaxp.SAXParserFactoryImpl;
45 import org.apache.xerces.util.XMLGrammarPoolImpl;
46 import org.jetbrains.annotations.NonNls;
47 import org.jetbrains.annotations.NotNull;
48 import org.xml.sax.InputSource;
49 import org.xml.sax.SAXException;
50 import org.xml.sax.SAXNotRecognizedException;
51 import org.xml.sax.SAXParseException;
52 import org.xml.sax.helpers.DefaultHandler;
54 import javax.swing.*;
55 import javax.xml.parsers.SAXParser;
56 import javax.xml.parsers.SAXParserFactory;
57 import java.io.FileNotFoundException;
58 import java.io.StringReader;
59 import java.net.*;
60 import java.util.ArrayList;
61 import java.util.HashSet;
62 import java.util.List;
63 import java.util.Set;
64 import java.util.concurrent.Future;
66 /**
67 * @author Mike
69 public class ValidateXmlActionHandler implements CodeInsightActionHandler {
70 private static final Logger LOG = Logger.getInstance("#com.intellij.xml.actions.ValidateXmlAction");
71 private static final Key<NewErrorTreeViewPanel> KEY = Key.create("ValidateXmlAction.KEY");
72 @NonNls private static final String SCHEMA_FULL_CHECKING_FEATURE_ID = "http://apache.org/xml/features/validation/schema-full-checking";
73 private static final String GRAMMAR_FEATURE_ID = Constants.XERCES_PROPERTY_PREFIX + Constants.XMLGRAMMAR_POOL_PROPERTY;
74 private static final Key<XMLGrammarPoolImpl> GRAMMAR_POOL_KEY = Key.create("GrammarPoolKey");
75 private static final Key<Long> GRAMMAR_POOL_TIME_STAMP_KEY = Key.create("GrammarPoolTimeStampKey");
76 private static final Key<VirtualFile[]> DEPENDENT_FILES_KEY = Key.create("GrammarPoolFilesKey");
78 private Project myProject;
79 private XmlFile myFile;
80 private ErrorReporter myErrorReporter;
81 private Object myParser;
82 private XmlResourceResolver myXmlResourceResolver;
83 private final boolean myForceChecking;
84 @NonNls
85 private static final String ENTITY_RESOLVER_PROPERTY_NAME = "http://apache.org/xml/properties/internal/entity-resolver";
86 @NonNls
87 public static final String XMLNS_PREFIX = "xmlns";
89 public ValidateXmlActionHandler(boolean _forceChecking) {
90 myForceChecking = _forceChecking;
93 public void setErrorReporter(ErrorReporter errorReporter) {
94 myErrorReporter = errorReporter;
97 public VirtualFile getFile(String publicId, String systemId) {
98 if (publicId == null) {
99 if (systemId != null) {
100 final String path = myXmlResourceResolver.getPathByPublicId(systemId);
101 if (path != null) return UriUtil.findRelativeFile(path,null);
102 final PsiFile file = myXmlResourceResolver.resolve(null, systemId);
103 if (file != null) return file.getVirtualFile();
105 return myFile.getVirtualFile();
107 final String path = myXmlResourceResolver.getPathByPublicId(publicId);
108 if (path != null) return UriUtil.findRelativeFile(path,null);
109 return null;
112 public abstract class ErrorReporter {
113 protected final Set<String> ourErrorsSet = new HashSet<String>();
114 public abstract void processError(SAXParseException ex,boolean warning);
116 public boolean filterValidationException(Exception ex) {
117 if (ex instanceof ProcessCanceledException) throw (ProcessCanceledException)ex;
118 if (ex instanceof XmlResourceResolver.IgnoredResourceException) throw (XmlResourceResolver.IgnoredResourceException)ex;
120 if (ex instanceof FileNotFoundException ||
121 ex instanceof MalformedURLException ||
122 ex instanceof NoRouteToHostException ||
123 ex instanceof SocketTimeoutException ||
124 ex instanceof UnknownHostException ||
125 ex instanceof ConnectException
127 // do not log problems caused by malformed and/or ignored external resources
128 return true;
131 if (ex instanceof NullPointerException) {
132 return true; // workaround for NPE at org.apache.xerces.impl.dtd.XMLDTDProcessor.checkDeclaredElements
135 return false;
138 public void startProcessing() {
139 doParse();
142 public boolean isStopOnUndeclaredResource() {
143 return false;
146 public boolean isUniqueProblem(final SAXParseException e) {
147 String error = buildMessageString(e);
148 if (ourErrorsSet.contains(error)) return false;
149 ourErrorsSet.add(error);
150 return true;
154 private String buildMessageString(SAXParseException ex) {
155 String msg = "(" + ex.getLineNumber() + ":" + ex.getColumnNumber() + ") " + ex.getMessage();
156 final VirtualFile file = getFile(ex.getPublicId(), ex.getSystemId());
158 if ( file != null && !file.equals(myFile.getVirtualFile())) {
159 msg = file.getName() + ":" + msg;
161 return msg;
164 public class TestErrorReporter extends ErrorReporter {
165 private final ArrayList<String> errors = new ArrayList<String>(3);
167 public boolean isStopOnUndeclaredResource() {
168 return true;
171 public boolean filterValidationException(final Exception ex) {
172 if (ex instanceof XmlResourceResolver.IgnoredResourceException) throw (XmlResourceResolver.IgnoredResourceException)ex;
173 return errors.add(ex.getMessage());
176 public void processError(SAXParseException ex, boolean warning) {
177 errors.add(buildMessageString(ex));
180 public List<String> getErrors() {
181 return errors;
185 class StdErrorReporter extends ErrorReporter {
186 private final NewErrorTreeViewPanel myErrorsView;
187 private final String CONTENT_NAME = XmlBundle.message("xml.validate.tab.content.title");
188 private boolean myErrorsDetected = false;
190 StdErrorReporter(Project project, Runnable rerunAction) {
191 myErrorsView = new NewErrorTreeViewPanel(project, null, true, true, rerunAction);
194 public void startProcessing() {
195 final Runnable task = new Runnable() {
196 public void run() {
197 try {
198 ApplicationManager.getApplication().runReadAction(new Runnable() {
199 public void run() {
200 StdErrorReporter.super.startProcessing();
204 SwingUtilities.invokeLater(
205 new Runnable() {
206 public void run() {
207 if (!myErrorsDetected) {
208 SwingUtilities.invokeLater(
209 new Runnable() {
210 public void run() {
211 removeCompileContents(null);
212 WindowManager.getInstance().getStatusBar(myProject).setInfo(
213 XmlBundle.message("xml.validate.no.errors.detected.status.message"));
222 finally {
223 boolean b = Thread.interrupted(); // reset interrupted
228 final MyProcessController processController = new MyProcessController();
229 myErrorsView.setProcessController(processController);
230 openMessageView();
231 processController.setFuture( ApplicationManager.getApplication().executeOnPooledThread(task) );
233 ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.MESSAGES_WINDOW).activate(null);
236 private void openMessageView() {
237 CommandProcessor commandProcessor = CommandProcessor.getInstance();
238 commandProcessor.executeCommand(
239 myProject, new Runnable() {
240 public void run() {
241 MessageView messageView = MessageView.SERVICE.getInstance(myProject);
242 final Content content = ContentFactory.SERVICE.getInstance().createContent(myErrorsView.getComponent(), CONTENT_NAME, true);
243 content.putUserData(KEY, myErrorsView);
244 messageView.getContentManager().addContent(content);
245 messageView.getContentManager().setSelectedContent(content);
246 messageView.getContentManager().addContentManagerListener(new CloseListener(content, messageView.getContentManager()));
247 removeCompileContents(content);
248 messageView.getContentManager().addContentManagerListener(new MyContentDisposer(content, messageView));
251 XmlBundle.message("validate.xml.open.message.view.command.name"),
252 null
255 private void removeCompileContents(Content notToRemove) {
256 MessageView messageView = MessageView.SERVICE.getInstance(myProject);
258 for (Content content : messageView.getContentManager().getContents()) {
259 if (content.isPinned()) continue;
260 if (CONTENT_NAME.equals(content.getDisplayName()) && content != notToRemove) {
261 ErrorTreeView listErrorView = (ErrorTreeView)content.getComponent();
262 if (listErrorView != null) {
263 if (messageView.getContentManager().removeContent(content, true)) {
264 content.release();
271 public void processError(final SAXParseException ex, final boolean warning) {
272 if (LOG.isDebugEnabled()) {
273 String error = buildMessageString(ex);
274 LOG.debug("enter: processError(error='" + error + "')");
277 myErrorsDetected = true;
279 if (!ApplicationManager.getApplication().isUnitTestMode()) {
280 SwingUtilities.invokeLater(
281 new Runnable() {
282 public void run() {
283 final VirtualFile file = getFile(ex.getPublicId(), ex.getSystemId());
284 myErrorsView.addMessage(
285 warning ? MessageCategory.WARNING : MessageCategory.ERROR,
286 new String[]{ex.getLocalizedMessage()},
287 file,
288 ex.getLineNumber() - 1,
289 ex.getColumnNumber() - 1,
290 null
298 private class CloseListener extends ContentManagerAdapter {
299 private Content myContent;
300 private final ContentManager myContentManager;
302 public CloseListener(Content content, ContentManager contentManager) {
303 myContent = content;
304 myContentManager = contentManager;
307 public void contentRemoved(ContentManagerEvent event) {
308 if (event.getContent() == myContent) {
309 myErrorsView.stopProcess();
311 myContentManager.removeContentManagerListener(this);
312 myContent.release();
313 myContent = null;
317 public void contentRemoveQuery(ContentManagerEvent event) {
318 if (event.getContent() == myContent) {
319 if (!myErrorsView.isProcessStopped()) {
320 int result = Messages.showYesNoDialog(
321 XmlBundle.message("xml.validate.validation.is.running.terminate.confirmation.text"),
322 XmlBundle.message("xml.validate.validation.is.running.terminate.confirmation.title"),
323 Messages.getQuestionIcon()
325 if (result != 0) {
326 event.consume();
333 private class MyProcessController implements NewErrorTreeViewPanel.ProcessController {
334 private Future<?> myFuture;
336 public void setFuture(Future<?> future) {
337 myFuture = future;
340 public void stopProcess() {
341 if (myFuture != null) {
342 myFuture.cancel(true);
346 public boolean isProcessStopped() {
347 return myFuture != null && myFuture.isDone();
352 public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
353 PsiDocumentManager.getInstance(project).commitAllDocuments();
355 doValidate(project,file);
358 public void doValidate(Project project, PsiFile file) {
359 myProject = project;
360 myFile = (XmlFile)file;
362 myXmlResourceResolver = new XmlResourceResolver(myFile, myProject, myErrorReporter);
363 myXmlResourceResolver.setStopOnUnDeclaredResource( myErrorReporter.isStopOnUndeclaredResource() );
365 try {
366 myParser = createParser();
368 if (myParser == null) return;
370 myErrorReporter.startProcessing();
372 catch (XmlResourceResolver.IgnoredResourceException e) {
374 catch (Exception exception) {
375 filterAppException(exception);
379 private void filterAppException(Exception exception) {
380 if (!myErrorReporter.filterValidationException(exception)) {
381 LOG.error(exception);
385 public boolean startInWriteAction() {
386 return true;
389 private void doParse() {
390 try {
391 if (myParser instanceof SAXParser) {
392 SAXParser parser = (SAXParser)myParser;
394 try {
395 parser.parse(new InputSource(new StringReader(myFile.getText())), new DefaultHandler() {
396 public void warning(SAXParseException e) {
397 if (myErrorReporter.isUniqueProblem(e)) myErrorReporter.processError(e, true);
400 public void error(SAXParseException e) {
401 if (myErrorReporter.isUniqueProblem(e)) myErrorReporter.processError(e, false);
404 public void fatalError(SAXParseException e) {
405 if (myErrorReporter.isUniqueProblem(e)) myErrorReporter.processError(e, false);
408 public InputSource resolveEntity(String publicId, String systemId) {
409 final PsiFile psiFile = myXmlResourceResolver.resolve(null, systemId);
410 if (psiFile == null) return null;
411 return new InputSource(new StringReader(psiFile.getText()));
414 public void startDocument() throws SAXException {
415 super.startDocument();
416 ((SAXParser)myParser).setProperty(
417 ENTITY_RESOLVER_PROPERTY_NAME,
418 myXmlResourceResolver
425 final String[] resourcePaths = myXmlResourceResolver.getResourcePaths();
426 if (resourcePaths.length > 0) { // if caches are used
427 final VirtualFile[] files = new VirtualFile[resourcePaths.length];
428 for(int i = 0; i < resourcePaths.length; ++i) {
429 files[i] = UriUtil.findRelativeFile(resourcePaths[i], null);
432 myFile.putUserData(DEPENDENT_FILES_KEY, files);
433 myFile.putUserData(GRAMMAR_POOL_TIME_STAMP_KEY, new Long(calculateTimeStamp(files,myProject)));
436 catch (SAXException e) {
437 LOG.debug(e);
438 // processError(e.getMessage(), false, 0, 0);
441 else {
442 LOG.error("unknown parser: " + myParser);
445 catch (Exception exception) {
446 filterAppException(exception);
447 } catch(StackOverflowError error) {
448 // http://issues.apache.org/jira/browse/XERCESJ-589
452 private Object createParser() {
453 try {
454 if (!needsDtdChecking() && !needsSchemaChecking() && !myForceChecking) {
455 return null;
458 SAXParserFactory factory = new SAXParserFactoryImpl();
459 boolean schemaChecking = false;
461 if (hasDtdDeclaration()) {
462 factory.setValidating(true);
465 if (needsSchemaChecking()) {
466 factory.setValidating(true);
467 factory.setNamespaceAware(true);
468 //jdk 1.5 API
469 try {
470 factory.setXIncludeAware(true);
471 } catch(NoSuchMethodError e) {}
472 schemaChecking = true;
475 SAXParser parser = factory.newSAXParser();
477 parser.setProperty(ENTITY_RESOLVER_PROPERTY_NAME, myXmlResourceResolver);
479 if (schemaChecking) { // when dtd checking schema refs could not be validated @see http://marc.theaimsgroup.com/?l=xerces-j-user&m=112504202423704&w=2
480 final XMLGrammarPoolImpl previousGrammarPool = myFile.getUserData(GRAMMAR_POOL_KEY);
481 XMLGrammarPoolImpl grammarPool = null;
483 // check if the pool is valid
484 if (!myForceChecking &&
485 !isValidationDependentFilesOutOfDate(myFile)
487 grammarPool = previousGrammarPool;
490 if (grammarPool == null) {
491 grammarPool = new XMLGrammarPoolImpl();
492 myFile.putUserData(GRAMMAR_POOL_KEY,grammarPool);
495 parser.getXMLReader().setProperty(GRAMMAR_FEATURE_ID, grammarPool);
498 try {
499 if (schemaChecking) {
500 parser.setProperty(JAXPConstants.JAXP_SCHEMA_LANGUAGE,JAXPConstants.W3C_XML_SCHEMA);
501 parser.getXMLReader().setFeature(SCHEMA_FULL_CHECKING_FEATURE_ID, true);
502 parser.getXMLReader().setFeature("http://apache.org/xml/features/honour-all-schemaLocations", true);
504 parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/warn-on-undeclared-elemdef",Boolean.TRUE);
505 parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/warn-on-duplicate-attdef",Boolean.TRUE);
508 parser.getXMLReader().setFeature("http://apache.org/xml/features/warn-on-duplicate-entitydef",Boolean.TRUE);
509 parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/unparsed-entity-checking",Boolean.FALSE);
510 } catch(SAXNotRecognizedException ex) {
511 // it is possible to continue work with configured parser
512 LOG.info("Xml parser installation seems screwed", ex);
515 return parser;
517 catch (Exception e) {
518 filterAppException(e);
521 return null;
524 public static boolean isValidationDependentFilesOutOfDate(XmlFile myFile) {
525 final VirtualFile[] files = myFile.getUserData(DEPENDENT_FILES_KEY);
526 final Long grammarPoolTimeStamp = myFile.getUserData(GRAMMAR_POOL_TIME_STAMP_KEY);
528 if (grammarPoolTimeStamp != null &&
529 files != null
531 long dependentFilesTimestamp = calculateTimeStamp(files,myFile.getProject());
533 if (dependentFilesTimestamp == grammarPoolTimeStamp.longValue() &&
534 dependentFilesTimestamp != 0
536 return false;
540 return true;
543 private static long calculateTimeStamp(final VirtualFile[] files, Project myProject) {
544 long timestamp = 0;
546 for(VirtualFile file:files) {
547 if (file == null || !file.isValid()) break;
548 final PsiFile psifile = PsiManager.getInstance(myProject).findFile(file);
550 if (psifile != null && psifile.isValid()) {
551 timestamp += psifile.getModificationStamp();
552 } else {
553 break;
556 return timestamp;
559 private boolean hasDtdDeclaration() {
560 XmlDocument document = myFile.getDocument();
561 if (document == null) return false;
562 XmlProlog prolog = document.getProlog();
563 if (prolog == null) return false;
564 XmlDoctype doctype = prolog.getDoctype();
565 if (doctype == null) return false;
567 return true;
570 private boolean needsDtdChecking() {
571 XmlDocument document = myFile.getDocument();
572 if (document == null) return false;
574 return (document.getProlog()!=null && document.getProlog().getDoctype()!=null);
577 private boolean needsSchemaChecking() {
578 XmlDocument document = myFile.getDocument();
579 if (document == null) return false;
580 XmlTag rootTag = document.getRootTag();
581 if (rootTag == null) return false;
583 XmlAttribute[] attributes = rootTag.getAttributes();
584 for (XmlAttribute attribute : attributes) {
585 if (attribute.isNamespaceDeclaration()) return true;
588 return false;
590 private static class MyContentDisposer implements ContentManagerListener {
591 private final Content myContent;
592 private final MessageView myMessageView;
594 public MyContentDisposer(final Content content, final MessageView messageView) {
595 myContent = content;
596 myMessageView = messageView;
599 public void contentRemoved(ContentManagerEvent event) {
600 final Content eventContent = event.getContent();
601 if (!eventContent.equals(myContent)) {
602 return;
604 myMessageView.getContentManager().removeContentManagerListener(this);
605 NewErrorTreeViewPanel errorTreeView = eventContent.getUserData(KEY);
606 if (errorTreeView != null) {
607 errorTreeView.dispose();
609 eventContent.putUserData(KEY, null);
612 public void contentAdded(ContentManagerEvent event) {
614 public void contentRemoveQuery(ContentManagerEvent event) {
616 public void selectionChanged(ContentManagerEvent event) {