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
;
55 import javax
.xml
.parsers
.SAXParser
;
56 import javax
.xml
.parsers
.SAXParserFactory
;
57 import java
.io
.FileNotFoundException
;
58 import java
.io
.StringReader
;
60 import java
.util
.ArrayList
;
61 import java
.util
.HashSet
;
62 import java
.util
.List
;
64 import java
.util
.concurrent
.Future
;
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
;
85 private static final String ENTITY_RESOLVER_PROPERTY_NAME
= "http://apache.org/xml/properties/internal/entity-resolver";
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);
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
131 if (ex
instanceof NullPointerException
) {
132 return true; // workaround for NPE at org.apache.xerces.impl.dtd.XMLDTDProcessor.checkDeclaredElements
138 public void startProcessing() {
142 public boolean isStopOnUndeclaredResource() {
146 public boolean isUniqueProblem(final SAXParseException e
) {
147 String error
= buildMessageString(e
);
148 if (ourErrorsSet
.contains(error
)) return false;
149 ourErrorsSet
.add(error
);
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
;
164 public class TestErrorReporter
extends ErrorReporter
{
165 private final ArrayList
<String
> errors
= new ArrayList
<String
>(3);
167 public boolean isStopOnUndeclaredResource() {
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() {
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() {
198 ApplicationManager
.getApplication().runReadAction(new Runnable() {
200 StdErrorReporter
.super.startProcessing();
204 SwingUtilities
.invokeLater(
207 if (!myErrorsDetected
) {
208 SwingUtilities
.invokeLater(
211 removeCompileContents(null);
212 WindowManager
.getInstance().getStatusBar(myProject
).setInfo(
213 XmlBundle
.message("xml.validate.no.errors.detected.status.message"));
223 boolean b
= Thread
.interrupted(); // reset interrupted
228 final MyProcessController processController
= new MyProcessController();
229 myErrorsView
.setProcessController(processController
);
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() {
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"),
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)) {
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(
283 final VirtualFile file
= getFile(ex
.getPublicId(), ex
.getSystemId());
284 myErrorsView
.addMessage(
285 warning ? MessageCategory
.WARNING
: MessageCategory
.ERROR
,
286 new String
[]{ex
.getLocalizedMessage()},
288 ex
.getLineNumber() - 1,
289 ex
.getColumnNumber() - 1,
298 private class CloseListener
extends ContentManagerAdapter
{
299 private Content myContent
;
300 private final ContentManager myContentManager
;
302 public CloseListener(Content content
, ContentManager contentManager
) {
304 myContentManager
= contentManager
;
307 public void contentRemoved(ContentManagerEvent event
) {
308 if (event
.getContent() == myContent
) {
309 myErrorsView
.stopProcess();
311 myContentManager
.removeContentManagerListener(this);
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()
333 private class MyProcessController
implements NewErrorTreeViewPanel
.ProcessController
{
334 private Future
<?
> myFuture
;
336 public void setFuture(Future
<?
> 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
) {
360 myFile
= (XmlFile
)file
;
362 myXmlResourceResolver
= new XmlResourceResolver(myFile
, myProject
, myErrorReporter
);
363 myXmlResourceResolver
.setStopOnUnDeclaredResource( myErrorReporter
.isStopOnUndeclaredResource() );
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() {
389 private void doParse() {
391 if (myParser
instanceof SAXParser
) {
392 SAXParser parser
= (SAXParser
)myParser
;
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
) {
438 // processError(e.getMessage(), false, 0, 0);
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() {
454 if (!needsDtdChecking() && !needsSchemaChecking() && !myForceChecking
) {
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);
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
);
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
);
517 catch (Exception e
) {
518 filterAppException(e
);
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 &&
531 long dependentFilesTimestamp
= calculateTimeStamp(files
,myFile
.getProject());
533 if (dependentFilesTimestamp
== grammarPoolTimeStamp
.longValue() &&
534 dependentFilesTimestamp
!= 0
543 private static long calculateTimeStamp(final VirtualFile
[] files
, Project myProject
) {
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();
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;
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;
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
) {
596 myMessageView
= messageView
;
599 public void contentRemoved(ContentManagerEvent event
) {
600 final Content eventContent
= event
.getContent();
601 if (!eventContent
.equals(myContent
)) {
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
) {