package de.uniba.dsg.bpmnspector.schematron; import java.io.File; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.transform.dom.DOMSource; import api.Location; import api.LocationCoordinate; import api.UnsortedValidationResult; import api.ValidationException; import api.ValidationResult; import api.Violation; import api.Warning; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import com.phloc.schematron.ISchematronResource; import com.phloc.schematron.pure.SchematronResourcePure; import de.uniba.dsg.bpmnspector.BpmnProcessValidator; import de.uniba.dsg.bpmnspector.common.importer.BPMNProcess; import de.uniba.dsg.bpmnspector.common.importer.ProcessImporter; import de.uniba.dsg.bpmnspector.common.util.ConstantHelper; import de.uniba.dsg.bpmnspector.schematron.preprocessing.PreProcessor; import org.jdom2.Element; import org.jdom2.Namespace; import org.jdom2.filter.Filters; import org.jdom2.located.LocatedElement; import org.jdom2.output.DOMOutputter; import org.jdom2.xpath.XPathFactory; import org.oclc.purl.dsdl.svrl.FailedAssert; import org.oclc.purl.dsdl.svrl.SchematronOutputType; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; /** * Does the validation process of the xsd and the schematron validation and * returns the results of the validation * * @author Philipp Neugebauer * @author Matthias Geiger * @version 1.0 */ public class SchematronBPMNValidator implements BpmnProcessValidator { private final PreProcessor preProcessor; private final ProcessImporter bpmnImporter; private final Ext002Checker ext002Checker; private final static Logger LOGGER; static { LOGGER = (Logger) LoggerFactory.getLogger(SchematronBPMNValidator.class .getSimpleName()); } { preProcessor = new PreProcessor(); bpmnImporter = new ProcessImporter(); ext002Checker = new Ext002Checker(); } public Level getLogLevel() { return LOGGER.getLevel(); } public void setLogLevel(Level logLevel) { // FIXME: without phloc libraries ((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)) .setLevel(logLevel); } public List<ValidationResult> validateFiles(List<File> xmlFiles) throws ValidationException { List<ValidationResult> validationResults = new ArrayList<>(); for (File xmlFile : xmlFiles) { validationResults.add(validate(xmlFile)); } return validationResults; } public ValidationResult validate(File xmlFile) throws ValidationException { ValidationResult validationResult = new UnsortedValidationResult(); // Trying to import process BPMNProcess process = bpmnImporter .importProcessFromPath(Paths.get(xmlFile.getPath()), validationResult); if(process != null) { validate(process, validationResult); } return validationResult; } public void validate(BPMNProcess process, ValidationResult validationResult) throws ValidationException { final List<ISchematronResource> schemaToCheck = loadAndValidateSchematronFiles(); LOGGER.info("Validating {}", process.getBaseURI()); try { // EXT.002 checks whether there are ID duplicates - as ID // duplicates in a single file are already detected during XSD // validation this is only relevant if other processes are imported if(!process.getChildren().isEmpty()) { ext002Checker.checkConstraint002(process, validationResult); } org.jdom2.Document documentToCheck = preProcessor.preProcess(process); DOMOutputter domOutputter = new DOMOutputter(); Document w3cDoc = domOutputter .output(documentToCheck); DOMSource domSource = new DOMSource(w3cDoc); for(ISchematronResource schematronFile : schemaToCheck) { SchematronOutputType schematronOutputType = schematronFile .applySchematronValidationToSVRL(domSource); schematronOutputType.getActivePatternAndFiredRuleAndFailedAssert().stream() .filter(obj -> obj instanceof FailedAssert) .forEach(obj -> handleSchematronErrors(process, validationResult, (FailedAssert) obj)); } } catch (Exception e) { // NOPMD LOGGER.debug("exception during schematron validation. Cause: {}", e); throw new ValidationException( "Something went wrong during schematron validation!"); } LOGGER.info("Validating process successfully done, file is valid: {}", validationResult.isValid()); } private List<ISchematronResource> loadAndValidateSchematronFiles() throws ValidationException { List<ISchematronResource> schemasToCheck = new ArrayList<>(); final ISchematronResource schematronSchemaDescriptive = SchematronResourcePure .fromClassPath("EXT_descriptive.sch"); if (!schematronSchemaDescriptive.isValidSchematron()) { LOGGER.debug("schematron file for Descriptive Conformance class is invalid"); throw new ValidationException("Invalid Schematron file (EXT_descriptive.sch)!"); } else { schemasToCheck.add(schematronSchemaDescriptive); } final ISchematronResource schematronSchemaAnalytic = SchematronResourcePure.fromClassPath("EXT_analytic.sch"); if(!schematronSchemaAnalytic.isValidSchematron()) { LOGGER.debug("schematron file for Analytic Conformance class is invalid"); throw new ValidationException("Invalid Schematron file (EXT_analytic.sch)!"); } else { schemasToCheck.add(schematronSchemaAnalytic); } final ISchematronResource schematronSchemaCommonExec = SchematronResourcePure.fromClassPath("EXT_commonExec.sch"); if(!schematronSchemaCommonExec.isValidSchematron()) { LOGGER.debug("schematron file for Common Executable Conformance class is invalid"); throw new ValidationException("Invalid Schematron file (EXT_commonExec.sch)!"); } else { schemasToCheck.add(schematronSchemaCommonExec); } final ISchematronResource schematronSchemaFull = SchematronResourcePure.fromClassPath("EXT_full.sch"); if(!schematronSchemaFull.isValidSchematron()) { LOGGER.debug("schematron file for Full Conformance class is invalid"); throw new ValidationException("Invalid Schematron file (EXT_full.sch)!"); } else { schemasToCheck.add(schematronSchemaFull); } return schemasToCheck; } /** * tries to locate errors in the specific files * * @param baseProcess * the file where the error must be located * @param validationResult * the result of the validation to add new found errors * @param failedAssert * the error of the schematron validation */ private void handleSchematronErrors(BPMNProcess baseProcess, ValidationResult validationResult, FailedAssert failedAssert) { String message = failedAssert.getText().trim(); String constraint = message.substring(0, message.indexOf('|')); String errorMessage = message.substring(message.indexOf('|') + 1); Location violationLocation = null; String location = ""; // direct usage of xpath expression failedAssert.getLocation() not possible // when using JDOM - predicates are not supported correctly. // Instead: determine the index of the element to be found and use this // accessor in the list of found elements Pattern p = Pattern.compile("\\[\\d+\\]"); Matcher m = p.matcher(failedAssert.getLocation()); int xpathIndex = 0; if(m.find()) { xpathIndex = Integer.parseInt( m.group(0).replace("[", "").replace("]", "")); location = failedAssert.getLocation().replace(m.group(0), ""); } LOGGER.debug("XPath to evaluate: "+location); XPathFactory fac = XPathFactory.instance(); List<Element> elems = fac.compile(location, Filters.element(), null, Namespace.getNamespace("bpmn", ConstantHelper.BPMNNAMESPACE)).evaluate( baseProcess.getProcessAsDoc()); LOGGER.debug("Number of found elems: "+elems.size()); String logText; if(elems.size()>=xpathIndex+1) { int line = ((LocatedElement) elems.get(xpathIndex)).getLine(); int column = ((LocatedElement) elems.get(xpathIndex)).getColumn(); String fileName = baseProcess.getBaseURI(); location = failedAssert.getLocation(); violationLocation = new Location( Paths.get(fileName).toAbsolutePath(), new LocationCoordinate(line, column), location); logText = String.format( "violation of constraint %s found in %s at line %s.", constraint, violationLocation.getResource(), violationLocation.getLocation().getRow()); } else { try { String xpathId = ""; if (failedAssert.getDiagnosticReferenceCount() > 0) { xpathId = failedAssert.getDiagnosticReference().get(0) .getText().trim(); } LOGGER.debug("Trying to locate in files: "+xpathId); violationLocation = searchForViolationFile(xpathId, baseProcess); logText = String.format( "violation of constraint %s found in %s at line %s.", constraint, violationLocation.getResource(), violationLocation.getLocation().getRow()); } catch (ValidationException | StringIndexOutOfBoundsException e) { LOGGER.error("Line of affected Element could not be determined."); logText = String.format("Found violation of constraint %s but the correct location could not be determined.", constraint); } } LOGGER.info(logText); if("EXT.076".equals(constraint)) { Warning warning = new Warning(errorMessage, violationLocation); validationResult.addWarning(warning); } else { Violation violation = new Violation(violationLocation, errorMessage, constraint); validationResult.addViolation(violation); } } /** * searches for the file and line, where the violation occured * * @param xpathExpression * the expression, through which the file and line should be * identified * @param baseProcess * baseProcess used for validation * @return the violation Location * @throws ValidationException * if no element can be found */ private Location searchForViolationFile(String xpathExpression, BPMNProcess baseProcess) throws ValidationException { String namespacePrefix = xpathExpression.substring(0, xpathExpression.indexOf('_')); Optional<BPMNProcess> optional = baseProcess.findProcessByGeneratedPrefix(namespacePrefix); if(optional.isPresent()) { String fileName = optional.get().getBaseURI(); int line = -1; int column = -1; // use ID with generated prefix for lookup String xpathObjectId = createIdBpmnExpression(xpathExpression); LOGGER.debug("Expression to evaluate: "+xpathObjectId); XPathFactory fac = XPathFactory.instance(); List<Element> elems = fac.compile(xpathObjectId, Filters.element(), null, Namespace.getNamespace("bpmn", ConstantHelper.BPMNNAMESPACE)).evaluate(optional.get().getProcessAsDoc()); if(elems.size()==1) { line = ((LocatedElement) elems.get(0)).getLine(); column = ((LocatedElement) elems.get(0)).getColumn(); //use ID without prefix (=original ID) as Violation xPath xpathObjectId = createIdBpmnExpression(xpathExpression .substring(xpathExpression.indexOf('_') + 1)); } if (line==-1 || column==-1) { throw new ValidationException("BPMN Element couldn't be found in file '"+fileName+"'!"); } return new Location(Paths.get(fileName).toAbsolutePath(), new LocationCoordinate(line, column), xpathObjectId); } else { // File not found throw new ValidationException("BPMN Element couldn't be found as no corresponding file could be found."); } } /** * creates a xpath expression for finding the id * * @param id * the id, to which the expression should refer * @return the xpath expression, which refers the given id */ private static String createIdBpmnExpression(String id) { return String.format("//bpmn:*[@id = '%s']", id); } }