| /******************************************************************************* |
| * Copyright (c) 2007 Chase Technology Ltd - http://www.chasetechnology.co.uk |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Doug Satchwell (Chase Technology Ltd) - initial API and implementation |
| * David Carver (STAR) - bug 230072 - Project level specific validation |
| * - bug 226245 - XPath 2.0 validation for XSLT |
| *******************************************************************************/ |
| package org.eclipse.wst.xsl.core.internal.validation; |
| |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import javax.xml.xpath.XPathExpressionException; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IMarker; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.ProjectScope; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; |
| import org.eclipse.wst.sse.core.internal.validate.ValidationMessage; |
| import org.eclipse.wst.xml.core.internal.validation.core.ValidationReport; |
| import org.eclipse.wst.xml.xpath.core.util.XPath20Helper; |
| import org.eclipse.wst.xml.xpath.core.util.XSLTXPathHelper; |
| import org.eclipse.wst.xsl.core.ValidationPreferences; |
| import org.eclipse.wst.xsl.core.XSLCore; |
| import org.eclipse.wst.xsl.core.internal.XSLCorePlugin; |
| import org.eclipse.wst.xsl.core.internal.util.Debug; |
| import org.eclipse.wst.xsl.core.model.CallTemplate; |
| import org.eclipse.wst.xsl.core.model.Include; |
| import org.eclipse.wst.xsl.core.model.Parameter; |
| import org.eclipse.wst.xsl.core.model.StylesheetModel; |
| import org.eclipse.wst.xsl.core.model.Template; |
| import org.eclipse.wst.xsl.core.model.XSLAttribute; |
| import org.eclipse.wst.xsl.core.model.XSLElement; |
| import org.eclipse.wst.xsl.core.model.XSLNode; |
| import org.eclipse.wst.xsl.core.Messages; |
| |
| |
| /** |
| * The XSL validator for workspace XSL files. |
| * |
| * @author Doug Satchwell |
| */ |
| public class XSLValidator |
| { |
| private static final String XSLT2_Version = "2.0"; //$NON-NLS-1$ |
| private static XSLValidator instance; |
| private IProject project; |
| |
| private XSLValidator() |
| { |
| } |
| |
| /** |
| * Validate the given XSL file. Same as <code>validate(xslFile,report,forceBuild)</code> except a new report is created and returned. |
| * |
| * @param xslFile the XSL file |
| * @param forceBuild true if build should always be forced |
| * @return the validation report |
| * @throws CoreException if any exception occurs while validating |
| */ |
| public ValidationReport validate(IFile xslFile, boolean forceBuild) throws CoreException |
| { |
| XSLValidationReport report = new XSLValidationReport(xslFile.getLocationURI().toString()); |
| project = xslFile.getProject(); |
| validate(xslFile, report, forceBuild); |
| return report; |
| } |
| |
| /** |
| * Validate the given XSL file using the specified report. |
| * |
| * @param xslFile the XSL file |
| * @param report the report to use for reporting validation errors |
| * @param forceBuild true if build should always be forced |
| * @return the validation report |
| * @throws CoreException if any exception occurs while validating |
| */ |
| public void validate(IFile xslFile, XSLValidationReport report, boolean forceBuild) throws CoreException |
| { |
| StylesheetModel stylesheet; |
| if (forceBuild) |
| stylesheet = XSLCore.getInstance().buildStylesheet(xslFile); |
| else |
| stylesheet = XSLCore.getInstance().getStylesheet(xslFile); |
| |
| project = xslFile.getProject(); |
| |
| long start; |
| if (Debug.debugXSLModel) { |
| start = System.currentTimeMillis(); |
| } |
| if (stylesheet!=null) |
| { |
| try |
| { |
| calculateProblems(stylesheet, report); |
| } |
| catch (MaxErrorsExceededException e) |
| { |
| // do nothing |
| } |
| } |
| if (Debug.debugXSLModel) { |
| long end = System.currentTimeMillis(); |
| System.out.println("VALIDATE "+xslFile+" in "+(end-start)+"ms"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } |
| } |
| |
| private void calculateProblems(StylesheetModel stylesheetComposed, XSLValidationReport report) throws MaxErrorsExceededException |
| { |
| // circular reference check |
| checkCircularRef(stylesheetComposed, report); |
| // include checks |
| checkIncludes(stylesheetComposed, report); |
| // template checks |
| checkTemplates(stylesheetComposed, report); |
| // call-template checks |
| checkCallTemplates(stylesheetComposed, report); |
| // call-template checks |
| if (getPreference(ValidationPreferences.XPATHS) > IMarker.SEVERITY_INFO) |
| checkXPaths(stylesheetComposed.getStylesheet(), report); |
| |
| // TODO a) check globals and b) apply-templates where mode does not exist |
| } |
| |
| private int getPreference(String key) |
| { |
| if (project == null) { |
| return XSLCorePlugin.getDefault().getPluginPreferences().getInt(key); |
| } |
| |
| IEclipsePreferences prefs = new ProjectScope(project).getNode(XSLCorePlugin.getDefault().getBundle().getSymbolicName()); |
| boolean useProject = prefs.getBoolean(XSLCorePlugin.USE_PROJECT_SETTINGS, false); |
| |
| int valPref; |
| if (useProject) { |
| valPref = prefs.getInt(key, ValidationMessage.WARNING); |
| } else { |
| valPref = XSLCorePlugin.getDefault().getPluginPreferences().getInt(key); |
| } |
| return valPref; |
| } |
| |
| private void checkXPaths(XSLElement xslEl, XSLValidationReport report) throws MaxErrorsExceededException |
| { |
| validateXPath(xslEl, report, "select"); //$NON-NLS-1$ |
| validateXPath(xslEl, report, "test"); //$NON-NLS-1$ |
| validateXPath(xslEl, report, "match"); //$NON-NLS-1$ |
| for (XSLElement childEl : xslEl.getChildElements()) |
| { |
| checkXPaths(childEl, report); |
| } |
| } |
| |
| private void validateXPath(XSLElement xslEl, XSLValidationReport report, String attName) throws MaxErrorsExceededException |
| { |
| XSLAttribute att = xslEl.getAttribute(attName); |
| if (att != null && att.getValue() != null) |
| { |
| try |
| { |
| String xslVersion = xslEl.getStylesheet().getVersion(); |
| String xpathExp = att.getValue(); |
| if (xslVersion.equals(XSLT2_Version)) { |
| XPath20Helper.compile(xpathExp); |
| } else { |
| XSLTXPathHelper.compile(att.getValue()); |
| } |
| } |
| catch (XPathExpressionException e) |
| { |
| createMarker(report, att, getPreference(ValidationPreferences.XPATHS), Messages.XSLValidator_1); |
| } |
| catch (NullPointerException e) |
| { |
| // not sure why NPE is being thrown here |
| } |
| } |
| } |
| |
| private void checkCircularRef(StylesheetModel stylesheetComposed, XSLValidationReport report) throws MaxErrorsExceededException |
| { |
| if (stylesheetComposed.hasCircularReference()) |
| createMarker(report, stylesheetComposed.getStylesheet(), getPreference(ValidationPreferences.CIRCULAR_REF), Messages.XSLValidator_2); |
| } |
| |
| private void checkIncludes(StylesheetModel stylesheetComposed, XSLValidationReport report) throws MaxErrorsExceededException |
| { |
| // includes |
| for (Include include : stylesheetComposed.getStylesheet().getIncludes()) |
| { |
| IFile includedFile = include.getHrefAsFile(); |
| if (includedFile == null || !includedFile.exists()) |
| { // included file does not exist |
| XSLAttribute att = include.getAttribute("href"); //$NON-NLS-1$ |
| if (att != null) |
| createMarker(report, att, getPreference(ValidationPreferences.MISSING_INCLUDE), Messages.XSLValidator_4 + include.getHref()); |
| else |
| createMarker(report, include, getPreference(ValidationPreferences.NAME_ATTRIBUTE_EMPTY), Messages.XSLValidator_23 ); |
| } |
| else if (includedFile.equals(include.getStylesheet().getFile())) |
| { // stylesheet including itself! |
| createMarker(report, include.getAttribute("href"), getPreference(ValidationPreferences.CIRCULAR_REF), Messages.XSLValidator_6); //$NON-NLS-1$ |
| } |
| } |
| //imports |
| for (Include include : stylesheetComposed.getStylesheet().getImports()) |
| { |
| IFile includedFile = include.getHrefAsFile(); |
| if (includedFile == null || !includedFile.exists()) |
| { // included file does not exist |
| createMarker(report, include.getAttribute("href"), getPreference(ValidationPreferences.MISSING_INCLUDE), Messages.XSLValidator_8 + include.getHref()); //$NON-NLS-1$ |
| } |
| else if (includedFile.equals(include.getStylesheet().getFile())) |
| { // stylesheet including itself! |
| createMarker(report, include.getAttribute("href"), getPreference(ValidationPreferences.CIRCULAR_REF), Messages.XSLValidator_10); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private void checkTemplates(StylesheetModel stylesheetComposed, XSLValidationReport report) throws MaxErrorsExceededException |
| { |
| for (Template template : stylesheetComposed.getStylesheet().getTemplates()) |
| { |
| // check attributes are correct |
| if (template.getName() != null) |
| {// named template |
| // if (template.getMatch() != null) |
| // createMarker(report, template, IMarker.SEVERITY_ERROR, "Template cannot specify both name and match attributes"); |
| // if (template.getMode() != null) |
| // createMarker(report, template, IMarker.SEVERITY_ERROR, "Named templates cannot specify a mode"); |
| checkParameters(report, template); |
| } |
| |
| for (Template checkTemplate : stylesheetComposed.getTemplates()) |
| { |
| if (checkTemplate != template && checkTemplate.equals(template)) |
| { |
| if (template.getStylesheet() == stylesheetComposed.getStylesheet() && checkTemplate.getStylesheet() == stylesheetComposed.getStylesheet()) |
| {// templates in this stylesheet conflict with each other |
| createMarker(report, template, getPreference(ValidationPreferences.TEMPLATE_CONFLICT), Messages.XSLValidator_11); |
| } |
| else if (template.getStylesheet() == stylesheetComposed.getStylesheet()) |
| {// template in included stylesheet conflicts with this |
| createMarker(report, template, getPreference(ValidationPreferences.TEMPLATE_CONFLICT), Messages.XSLValidator_12); |
| } |
| else |
| {// templates in included stylesheets conflict with each other |
| createMarker(report, template.getStylesheet(), getPreference(ValidationPreferences.TEMPLATE_CONFLICT), Messages.XSLValidator_13); |
| } |
| } |
| } |
| } |
| } |
| |
| private void checkParameters(XSLValidationReport report, Template template) throws MaxErrorsExceededException |
| { |
| List<Parameter> parameters = new ArrayList<Parameter>(template.getParameters()); |
| // reverse the parameters order for checking - for duplicate parameters |
| // the first one is valid |
| Collections.reverse(parameters); |
| Set<Parameter> duplicateParameters = new HashSet<Parameter>(); |
| // check parameters |
| for (Parameter param : parameters) |
| { |
| if (param.getName() == null) |
| {// name is required |
| createMarker(report, param, getPreference(ValidationPreferences.NAME_ATTRIBUTE_MISSING), Messages.XSLValidator_14); |
| } |
| else if (param.getName().trim().length() == 0) |
| {// name value is required |
| createMarker(report, param, getPreference(ValidationPreferences.NAME_ATTRIBUTE_EMPTY), Messages.XSLValidator_15); |
| } |
| else if (duplicateParameters.contains(param)) |
| {// don't recheck the parameter |
| continue; |
| } |
| else |
| {// check a parameter with the same name does not exist |
| for (Parameter checkParam : parameters) |
| { |
| if (param != checkParam) |
| { |
| if (param.getName().equals(checkParam.getName())) |
| { |
| duplicateParameters.add(checkParam); |
| createMarker(report, param, getPreference(ValidationPreferences.DUPLICATE_PARAMETER), Messages.XSLValidator_16); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void checkCallTemplates(StylesheetModel stylesheetComposed, XSLValidationReport report) throws MaxErrorsExceededException |
| { |
| for (CallTemplate calledTemplate : stylesheetComposed.getStylesheet().getCalledTemplates()) |
| { |
| if (calledTemplate.getName() != null) |
| { |
| // get the list of templates that might be being called by this |
| // template call |
| List<Template> templateList = stylesheetComposed.getTemplatesByName(calledTemplate.getName()); |
| if (templateList.size() == 0) |
| { |
| Object[] messageArgs = { calledTemplate.getName() }; |
| createMarker(report, calledTemplate.getAttribute("name"), getPreference(ValidationPreferences.CALL_TEMPLATES), MessageFormat.format(Messages.XSLValidator_18, messageArgs)); //$NON-NLS-1$ |
| } |
| else |
| { |
| Template namedTemplate = templateList.get(0); |
| for (Parameter calledTemplateParam : calledTemplate.getParameters()) |
| { |
| boolean found = false; |
| for (Parameter namedTemplateParam : namedTemplate.getParameters()) |
| { |
| if (calledTemplateParam.getName().equals(namedTemplateParam.getName())) |
| { |
| found = true; |
| if (!namedTemplateParam.isValue() && !calledTemplateParam.isValue()) { |
| Object[] messageArgs = { calledTemplateParam.getName() }; |
| createMarker(report, calledTemplateParam, getPreference(ValidationPreferences.EMPTY_PARAM), MessageFormat.format(Messages.XSLValidator_20, messageArgs)); |
| } |
| break; |
| |
| } |
| } |
| if (!found) { |
| Object[] messageArgs = { calledTemplateParam.getName() }; |
| createMarker(report, calledTemplateParam.getAttribute("name"), getPreference(ValidationPreferences.MISSING_PARAM), MessageFormat.format(Messages.XSLValidator_22, messageArgs)); //$NON-NLS-1$ |
| } |
| } |
| if (getPreference(ValidationPreferences.MISSING_PARAM) > IMarker.SEVERITY_INFO) |
| { |
| for (Parameter namedTemplateParam : namedTemplate.getParameters()) |
| { |
| if (!namedTemplateParam.isValue()) |
| { |
| boolean found = false; |
| for (Parameter calledTemplateParam : calledTemplate.getParameters()) |
| { |
| if (calledTemplateParam.getName().equals(namedTemplateParam.getName())) |
| { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| Object[] messageArgs = { namedTemplateParam.getName() }; |
| createMarker(report, calledTemplate, getPreference(ValidationPreferences.MISSING_PARAM), MessageFormat.format(Messages.XSLValidator_3, messageArgs)); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void createMarker(XSLValidationReport report, XSLNode xslNode, int severity, String message) throws MaxErrorsExceededException |
| { |
| if (severity > IMarker.SEVERITY_INFO) |
| { |
| if (report.getErrors().size() + report.getWarnings().size() > getPreference(ValidationPreferences.MAX_ERRORS)) |
| throw new MaxErrorsExceededException(); |
| switch (severity) |
| { |
| case IMarker.SEVERITY_ERROR: |
| report.addError(xslNode, message); |
| break; |
| case IMarker.SEVERITY_WARNING: |
| report.addWarning(xslNode, message); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Get the singleton XSLValidator instance. |
| * |
| * @return the singleton XSLValidator instance |
| */ |
| public static XSLValidator getInstance() |
| { |
| if (instance == null) |
| instance = new XSLValidator(); |
| return instance; |
| } |
| } |