| /* |
| * Copyright 2000-2014 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.actions.validate; |
| |
| import com.intellij.javaee.UriUtil; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.PsiManager; |
| import com.intellij.psi.xml.*; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.Function; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.xml.util.XmlResourceResolver; |
| import org.apache.xerces.impl.Constants; |
| import org.apache.xerces.jaxp.JAXPConstants; |
| import org.apache.xerces.jaxp.SAXParserFactoryImpl; |
| import org.apache.xerces.util.XMLGrammarPoolImpl; |
| import org.apache.xerces.xni.grammars.XMLGrammarPool; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.Nullable; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.SAXNotRecognizedException; |
| import org.xml.sax.SAXParseException; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| import java.io.StringReader; |
| import java.util.Arrays; |
| |
| /** |
| * @author Mike |
| */ |
| public class ValidateXmlActionHandler { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.xml.actions.validate.ValidateXmlAction"); |
| @NonNls private static final String SCHEMA_FULL_CHECKING_FEATURE_ID = "http://apache.org/xml/features/validation/schema-full-checking"; |
| private static final String GRAMMAR_FEATURE_ID = Constants.XERCES_PROPERTY_PREFIX + Constants.XMLGRAMMAR_POOL_PROPERTY; |
| |
| private static final Key<XMLGrammarPool> GRAMMAR_POOL_KEY = Key.create("GrammarPoolKey"); |
| private static final Key<Long> GRAMMAR_POOL_TIME_STAMP_KEY = Key.create("GrammarPoolTimeStampKey"); |
| private static final Key<VirtualFile[]> DEPENDENT_FILES_KEY = Key.create("GrammarPoolFilesKey"); |
| private static final Key<String[]> KNOWN_NAMESPACES_KEY = Key.create("KnownNamespacesKey"); |
| |
| private Project myProject; |
| private XmlFile myFile; |
| private ErrorReporter myErrorReporter; |
| private SAXParser myParser; |
| private XmlResourceResolver myXmlResourceResolver; |
| private final boolean myForceChecking; |
| @NonNls |
| private static final String ENTITY_RESOLVER_PROPERTY_NAME = "http://apache.org/xml/properties/internal/entity-resolver"; |
| |
| public ValidateXmlActionHandler(boolean _forceChecking) { |
| myForceChecking = _forceChecking; |
| } |
| |
| public void setErrorReporter(ErrorReporter errorReporter) { |
| myErrorReporter = errorReporter; |
| } |
| |
| public VirtualFile getFile(String publicId, String systemId) { |
| if (publicId == null) { |
| if (systemId != null) { |
| final String path = myXmlResourceResolver.getPathByPublicId(systemId); |
| if (path != null) return UriUtil.findRelativeFile(path,null); |
| final PsiFile file = myXmlResourceResolver.resolve(null, systemId); |
| if (file != null) return file.getVirtualFile(); |
| } |
| return myFile.getVirtualFile(); |
| } |
| final String path = myXmlResourceResolver.getPathByPublicId(publicId); |
| if (path != null) return UriUtil.findRelativeFile(path,null); |
| return null; |
| } |
| |
| public enum ProblemType { WARNING, ERROR, FATAL } |
| |
| public String buildMessageString(SAXParseException ex) { |
| String msg = "(" + ex.getLineNumber() + ":" + ex.getColumnNumber() + ") " + ex.getMessage(); |
| final VirtualFile file = getFile(ex.getPublicId(), ex.getSystemId()); |
| |
| if ( file != null && !file.equals(myFile.getVirtualFile())) { |
| msg = file.getName() + ":" + msg; |
| } |
| return msg; |
| } |
| |
| public void doValidate(XmlFile file) { |
| myProject = file.getProject(); |
| myFile = file; |
| |
| myXmlResourceResolver = new XmlResourceResolver(myFile, myProject, myErrorReporter); |
| myXmlResourceResolver.setStopOnUnDeclaredResource( myErrorReporter.isStopOnUndeclaredResource() ); |
| |
| try { |
| try { |
| myParser = createParser(); |
| } |
| catch (Exception e) { |
| filterAppException(e); |
| } |
| |
| if (myParser == null) return; |
| |
| myErrorReporter.startProcessing(); |
| } |
| catch (XmlResourceResolver.IgnoredResourceException ignore) { |
| } |
| catch (Exception exception) { |
| filterAppException(exception); |
| } |
| } |
| |
| private void filterAppException(Exception exception) { |
| if (!myErrorReporter.filterValidationException(exception)) { |
| LOG.error(exception); |
| } |
| } |
| |
| public void doParse() { |
| try { |
| myParser.parse(new InputSource(new StringReader(myFile.getText())), new DefaultHandler() { |
| @Override |
| public void warning(SAXParseException e) throws SAXException { |
| if (myErrorReporter.isUniqueProblem(e)) myErrorReporter.processError(e, ProblemType.WARNING); |
| } |
| |
| @Override |
| public void error(SAXParseException e) throws SAXException { |
| if (myErrorReporter.isUniqueProblem(e)) myErrorReporter.processError(e, ProblemType.ERROR); |
| } |
| |
| @Override |
| public void fatalError(SAXParseException e) throws SAXException { |
| if (myErrorReporter.isUniqueProblem(e)) myErrorReporter.processError(e, ProblemType.FATAL); |
| } |
| |
| @Override |
| public InputSource resolveEntity(String publicId, String systemId) { |
| final PsiFile psiFile = myXmlResourceResolver.resolve(null, systemId); |
| if (psiFile == null) return null; |
| return new InputSource(new StringReader(psiFile.getText())); |
| } |
| |
| @Override |
| public void startDocument() throws SAXException { |
| super.startDocument(); |
| myParser.setProperty( |
| ENTITY_RESOLVER_PROPERTY_NAME, |
| myXmlResourceResolver |
| ); |
| } |
| }); |
| |
| final String[] resourcePaths = myXmlResourceResolver.getResourcePaths(); |
| if (resourcePaths.length > 0) { // if caches are used |
| final VirtualFile[] files = new VirtualFile[resourcePaths.length]; |
| for (int i = 0; i < resourcePaths.length; ++i) { |
| files[i] = UriUtil.findRelativeFile(resourcePaths[i], null); |
| } |
| |
| myFile.putUserData(DEPENDENT_FILES_KEY, files); |
| myFile.putUserData(GRAMMAR_POOL_TIME_STAMP_KEY, calculateTimeStamp(files, myProject)); |
| } |
| myFile.putUserData(KNOWN_NAMESPACES_KEY, getNamespaces(myFile)); |
| } |
| catch (SAXException e) { |
| LOG.debug(e); |
| } |
| catch (Exception exception) { |
| filterAppException(exception); |
| } |
| catch (StackOverflowError error) { |
| // http://issues.apache.org/jira/browse/XERCESJ-589 |
| } |
| } |
| |
| protected SAXParser createParser() throws SAXException, ParserConfigurationException { |
| if (!needsDtdChecking() && !needsSchemaChecking() && !myForceChecking) { |
| return null; |
| } |
| |
| SAXParserFactory factory = new SAXParserFactoryImpl(); |
| boolean schemaChecking = false; |
| |
| if (hasDtdDeclaration()) { |
| factory.setValidating(true); |
| } |
| |
| if (needsSchemaChecking()) { |
| factory.setValidating(true); |
| factory.setNamespaceAware(true); |
| //jdk 1.5 API |
| try { |
| factory.setXIncludeAware(true); |
| } catch(NoSuchMethodError ignore) {} |
| schemaChecking = true; |
| } |
| |
| SAXParser parser = factory.newSAXParser(); |
| |
| parser.setProperty(ENTITY_RESOLVER_PROPERTY_NAME, myXmlResourceResolver); |
| |
| if (schemaChecking) { // when dtd checking schema refs could not be validated @see http://marc.theaimsgroup.com/?l=xerces-j-user&m=112504202423704&w=2 |
| XMLGrammarPool grammarPool = getGrammarPool(myFile, myForceChecking); |
| |
| parser.getXMLReader().setProperty(GRAMMAR_FEATURE_ID, grammarPool); |
| } |
| |
| try { |
| if (schemaChecking) { |
| parser.setProperty(JAXPConstants.JAXP_SCHEMA_LANGUAGE,JAXPConstants.W3C_XML_SCHEMA); |
| parser.getXMLReader().setFeature(SCHEMA_FULL_CHECKING_FEATURE_ID, true); |
| |
| if (Boolean.TRUE.equals(Boolean.getBoolean(XmlResourceResolver.HONOUR_ALL_SCHEMA_LOCATIONS_PROPERTY_KEY))) { |
| parser.getXMLReader().setFeature("http://apache.org/xml/features/honour-all-schemaLocations", true); |
| } |
| |
| parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/warn-on-undeclared-elemdef",Boolean.TRUE); |
| parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/warn-on-duplicate-attdef",Boolean.TRUE); |
| } |
| |
| parser.getXMLReader().setFeature("http://apache.org/xml/features/warn-on-duplicate-entitydef",Boolean.TRUE); |
| parser.getXMLReader().setFeature("http://apache.org/xml/features/validation/unparsed-entity-checking",Boolean.FALSE); |
| } catch(SAXNotRecognizedException ex) { |
| // it is possible to continue work with configured parser |
| LOG.info("Xml parser installation seems screwed", ex); |
| } |
| |
| return parser; |
| } |
| |
| public static XMLGrammarPool getGrammarPool(XmlFile file, boolean forceChecking) { |
| final XMLGrammarPool previousGrammarPool = getGrammarPool(file); |
| XMLGrammarPool grammarPool = null; |
| |
| // check if the pool is valid |
| if (!forceChecking && !isValidationDependentFilesOutOfDate(file)) { |
| grammarPool = previousGrammarPool; |
| } |
| |
| if (grammarPool == null) { |
| grammarPool = new XMLGrammarPoolImpl(); |
| file.putUserData(GRAMMAR_POOL_KEY,grammarPool); |
| } |
| return grammarPool; |
| } |
| |
| @Nullable |
| public static XMLGrammarPool getGrammarPool(XmlFile file) { |
| return file.getUserData(GRAMMAR_POOL_KEY); |
| } |
| |
| public static boolean isValidationDependentFilesOutOfDate(XmlFile myFile) { |
| final VirtualFile[] files = myFile.getUserData(DEPENDENT_FILES_KEY); |
| final Long grammarPoolTimeStamp = myFile.getUserData(GRAMMAR_POOL_TIME_STAMP_KEY); |
| String[] ns = myFile.getUserData(KNOWN_NAMESPACES_KEY); |
| |
| if (!Arrays.equals(ns, getNamespaces(myFile))) { |
| return true; |
| } |
| |
| if (grammarPoolTimeStamp != null && files != null) { |
| long dependentFilesTimestamp = calculateTimeStamp(files,myFile.getProject()); |
| |
| if (dependentFilesTimestamp == grammarPoolTimeStamp.longValue()) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| private static String[] getNamespaces(XmlFile file) { |
| XmlTag rootTag = file.getRootTag(); |
| if (rootTag == null) return ArrayUtil.EMPTY_STRING_ARRAY; |
| return ContainerUtil.mapNotNull(rootTag.getAttributes(), new Function<XmlAttribute, String>() { |
| @Override |
| public String fun(XmlAttribute attribute) { |
| return attribute.getValue(); |
| } |
| }, ArrayUtil.EMPTY_STRING_ARRAY); |
| } |
| |
| private static long calculateTimeStamp(final VirtualFile[] files, Project myProject) { |
| long timestamp = 0; |
| |
| for(VirtualFile file:files) { |
| if (file == null || !file.isValid()) break; |
| final PsiFile psifile = PsiManager.getInstance(myProject).findFile(file); |
| |
| if (psifile != null && psifile.isValid()) { |
| timestamp += psifile.getViewProvider().getModificationStamp(); |
| } else { |
| break; |
| } |
| } |
| return timestamp; |
| } |
| |
| private boolean hasDtdDeclaration() { |
| XmlDocument document = myFile.getDocument(); |
| if (document == null) return false; |
| XmlProlog prolog = document.getProlog(); |
| if (prolog == null) return false; |
| XmlDoctype doctype = prolog.getDoctype(); |
| if (doctype == null) return false; |
| |
| return true; |
| } |
| |
| private boolean needsDtdChecking() { |
| XmlDocument document = myFile.getDocument(); |
| if (document == null) return false; |
| |
| return (document.getProlog()!=null && document.getProlog().getDoctype()!=null); |
| } |
| |
| private boolean needsSchemaChecking() { |
| XmlDocument document = myFile.getDocument(); |
| if (document == null) return false; |
| XmlTag rootTag = document.getRootTag(); |
| if (rootTag == null) return false; |
| |
| XmlAttribute[] attributes = rootTag.getAttributes(); |
| for (XmlAttribute attribute : attributes) { |
| if (attribute.isNamespaceDeclaration()) return true; |
| } |
| |
| return false; |
| } |
| } |