-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Schematron validation using Saxon Home Edition (Java) #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9a5d65d
f17f3f3
dfa0aad
da2919d
d907505
3c24ad1
399930d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Use PHP 8.3 CLI as base image | ||
| FROM php:8.3-cli | ||
|
|
||
| # Install system dependencies and PHP extensions | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends \ | ||
| git \ | ||
| unzip \ | ||
| curl \ | ||
| ca-certificates \ | ||
| libzip-dev \ | ||
| libicu-dev \ | ||
| libxml2-dev \ | ||
| default-jre \ | ||
| wget \ | ||
| && docker-php-ext-install intl zip dom xml \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Install Composer | ||
| COPY --from=composer:2 /usr/bin/composer /usr/bin/composer | ||
|
|
||
| # Set working directory | ||
| WORKDIR /app | ||
|
|
||
| # Install Saxon Home edition for Java | ||
| RUN wget -nv "https://github.com/Saxonica/Saxon-HE/releases/download/SaxonHE12-9/SaxonHE12-9J.zip" -O /tmp/saxon.zip && \ | ||
| unzip /tmp/saxon.zip -d /opt/saxon && \ | ||
| rm /tmp/saxon.zip | ||
|
|
||
| ENV PATH="/opt/saxon/bin:${PATH}" | ||
|
|
||
| ENV SAXON_JAR="/opt/saxon/saxon-he-12.9.jar" | ||
|
|
||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,8 @@ | |
| "friendsofphp/php-cs-fixer": "^3.93", | ||
| "phpunit/phpunit": "^12.5", | ||
| "ext-libxml": "*", | ||
| "ext-dom": "*" | ||
| "ext-dom": "*", | ||
| "symfony/process": "^7.4" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normal que ce soit qu'en require-dev ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oui cela permet d'utiliser la lib même si on n'a pas besoin du validateur de schematron (et donc de symfony/process) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Et du coup pourquoi avoir mis le schematron validator dans la lib et pas dans charon, pour Tiime ? parce qu'on a besoin du schematron validator pour d'autre flux que les F6 ... On ne va pas le mettre dans toutes les libs ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oui c'est pour l'usage côté Tiime, maintenant il est vrai qu'extraire une lib dédiée indépendante pour la validation de schematron peut etre une idée intéressante dans le futur. |
||
| }, | ||
| "scripts": { | ||
| "cs-fix": "php-cs-fixer fix", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # SaxonJarSchematronValidator | ||
|
|
||
| The `SaxonJarSchematronValidator` is a Schematron validator implementation that uses the Saxon XSLT processor (Java edition) to validate XML documents against Schematron rules compiled as XSLT stylesheets. | ||
|
|
||
| ## Overview | ||
|
|
||
| Schematron is a rule-based validation language for XML documents. Unlike XSD (XML Schema Definition), which validates structure, Schematron validates business rules and constraints using XPath expressions. | ||
|
|
||
| In this implementation: | ||
| 1. Schematron rules are pre-compiled into an XSLT stylesheet | ||
| 2. Saxon XSLT processor transforms the XML document using the XSLT rules | ||
| 3. The output is an SVRL (Schematron Validation Report Language) document | ||
| 4. Failed assertions are extracted and reported as validation errors | ||
|
|
||
| ## Requirements | ||
|
|
||
| ### System Requirements | ||
|
|
||
| - **Java Runtime Environment (JRE)** 8 or higher | ||
| - The `java` command must be available in your system PATH | ||
| - Check with: `java -version` | ||
|
|
||
| - **Saxon-HE JAR file** (Home Edition, version 9.x or higher) | ||
| - Download from: [Saxonica Downloads](https://www.saxonica.com/download/java.xml) | ||
|
|
||
| ### PHP Requirements | ||
|
|
||
| - **PHP 8.3** or higher | ||
| - **Required PHP extensions:** | ||
| - `dom` - for parsing SVRL XML output | ||
| - `libxml` - for XML handling | ||
|
|
||
| ### Composer Dependencies | ||
|
|
||
| ```bash | ||
| composer require symfony/process | ||
| ``` | ||
|
|
||
| The `symfony/process` component is used to execute the Java Saxon processor as a subprocess. | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Basic Usage | ||
|
|
||
| ```php | ||
| <?php | ||
|
|
||
| use TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron\SaxonJarSchematronValidator; | ||
| use TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron\ValidationFailedException; | ||
|
|
||
| // Create validator instance with path to Saxon JAR | ||
| $validator = new SaxonJarSchematronValidator( | ||
| saxonJar: '/usr/local/lib/saxon-he.jar' | ||
| ); | ||
|
|
||
| try { | ||
| // Validate XML file against Schematron XSLT rules | ||
| $validator->validate( | ||
| xmlFilepath: '/path/to/document.xml', | ||
| xsltFilepath: '/path/to/schematron-rules.xsl' | ||
| ); | ||
|
|
||
| echo "Validation successful!\n"; | ||
|
|
||
| } catch (ValidationFailedException $e) { | ||
| echo "Validation failed: " . $e->getMessage() . "\n"; | ||
|
|
||
| // Access validation errors | ||
| foreach ($e->errors as $error) { | ||
| echo sprintf( | ||
| "Error [%s]: %s\n Location: %s\n Test: %s\n", | ||
| $error->getId(), | ||
| $error->getText(), | ||
| $error->getLocation(), | ||
| $error->getTest() | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||
|
|
||
| use Symfony\Component\Process\Process; | ||
|
|
||
| final readonly class SaxonJarSchematronValidator implements SchematronValidatorInterface | ||
| { | ||
| /** | ||
| * @throws \LogicException | ||
| */ | ||
| public function __construct(private string $saxonJar) | ||
| { | ||
| if (false === class_exists(Process::class)) { | ||
| throw new \LogicException('Symfony Process component is required to use SaxonJarSchematronValidator. Run "composer require symfony/process"'); | ||
| } | ||
|
|
||
| if (false === extension_loaded('dom') || false === extension_loaded('libxml')) { | ||
| throw new \LogicException('DOM and Libxml extensions are required to validate business rules.'); | ||
| } | ||
|
qdequippe marked this conversation as resolved.
|
||
|
|
||
| if (false === is_file($this->saxonJar) || false === is_readable($this->saxonJar)) { | ||
| throw new \LogicException(sprintf('Saxon JAR file "%s" does not exist or is not readable.', $this->saxonJar)); | ||
| } | ||
| } | ||
|
|
||
| public function validate(string $xmlFilepath, string $xsltFilepath): void | ||
| { | ||
| $process = new Process([ | ||
| 'java', | ||
| '-jar', | ||
| $this->saxonJar, | ||
| '-s:'.$xmlFilepath, | ||
| '-xsl:'.$xsltFilepath, | ||
| ]); | ||
|
qdequippe marked this conversation as resolved.
|
||
|
|
||
| $process->setTimeout(3600); | ||
|
qdequippe marked this conversation as resolved.
|
||
| $process->run(); | ||
|
|
||
| if (false === $process->isSuccessful()) { | ||
| throw new ValidationFailedException(message: $process->getErrorOutput(), code: $process->getExitCode() ?? 1); | ||
| } | ||
|
|
||
| $output = trim($process->getOutput()); | ||
|
|
||
| $doc = new \DOMDocument(); | ||
|
|
||
| try { | ||
|
qdequippe marked this conversation as resolved.
|
||
| if (false === $doc->loadXML($output)) { | ||
| throw new ValidationFailedException(message: 'Failed to parse Schematron validation output: invalid XML.'); | ||
| } | ||
| } catch (\Throwable $throwable) { | ||
| throw new ValidationFailedException(message: 'Failed to parse Schematron validation output: '.$throwable->getMessage(), previous: $throwable); | ||
| } | ||
|
|
||
| $xpath = new \DOMXPath($doc); | ||
| $xpath->registerNamespace('svrl', 'http://purl.oclc.org/dsdl/svrl'); | ||
|
|
||
| $failedAsserts = $xpath->query('//svrl:failed-assert'); | ||
|
|
||
| if (false === $failedAsserts) { | ||
| throw new ValidationFailedException(message: 'Failed to parse Schematron validation output'); | ||
| } | ||
|
|
||
| $errors = []; | ||
|
|
||
| /** @var \DOMNode $fa */ | ||
| foreach ($failedAsserts as $fa) { | ||
| if (!$fa instanceof \DOMElement) { | ||
| continue; | ||
| } | ||
|
|
||
| $location = $fa->getAttribute('location'); | ||
| $test = $fa->getAttribute('test'); | ||
|
|
||
| $text = null; | ||
| $textElements = $fa->getElementsByTagName('text'); | ||
| if (0 !== $textElements->length && $textElements->item(0) instanceof \DOMElement) { | ||
| $text = $textElements->item(0)->nodeValue; | ||
| } | ||
|
|
||
| $errors[] = new ValidationError( | ||
| test: $test, | ||
| id: $fa->getAttribute('id'), | ||
| flag: $fa->getAttribute('flag'), | ||
| location: $location, | ||
| text: $text, | ||
| ); | ||
| } | ||
|
|
||
|
qdequippe marked this conversation as resolved.
|
||
| if ([] === $errors) { | ||
| return; | ||
| } | ||
|
|
||
| throw new ValidationFailedException(errors: $errors, message: 'Schematron validation failed'); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||
|
|
||
| interface SchematronValidatorInterface | ||
| { | ||
| /** | ||
| * @param string $xmlFilepath the path to the XML file to validate against the business rules | ||
| * @param string $xsltFilepath the path to the Schematron XSLT file to use for validation | ||
| * | ||
| * @throws ValidationFailedException | ||
| */ | ||
| public function validate(string $xmlFilepath, string $xsltFilepath): void; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||
|
|
||
| final readonly class ValidationError | ||
|
qdequippe marked this conversation as resolved.
|
||
| { | ||
| private string $test; | ||
|
|
||
| private string $id; | ||
|
|
||
| private string $flag; | ||
|
|
||
| private string $location; | ||
|
|
||
| private ?string $text; | ||
|
|
||
| public function __construct( | ||
| string $test, | ||
| string $id, | ||
| string $flag, | ||
| string $location, | ||
| ?string $text, | ||
| ) { | ||
| $this->test = $test; | ||
| $this->id = $id; | ||
| $this->flag = $flag; | ||
| $this->location = $location; | ||
| $this->text = $text; | ||
| } | ||
|
qdequippe marked this conversation as resolved.
|
||
|
|
||
| public function getTest(): string | ||
| { | ||
| return $this->test; | ||
| } | ||
|
|
||
| public function getId(): string | ||
| { | ||
| return $this->id; | ||
| } | ||
|
|
||
| public function getFlag(): string | ||
| { | ||
| return $this->flag; | ||
| } | ||
|
|
||
| public function getLocation(): string | ||
| { | ||
| return $this->location; | ||
| } | ||
|
|
||
| public function getText(): ?string | ||
| { | ||
| return $this->text; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace TiimePDP\CrossDomainAcknowledgementAndResponse\Schematron; | ||
|
|
||
| final class ValidationFailedException extends \RuntimeException | ||
|
qdequippe marked this conversation as resolved.
|
||
| { | ||
| /** | ||
| * @param ValidationError[] $errors | ||
| */ | ||
| public function __construct( | ||
| public readonly array $errors = [], | ||
| string $message = '', | ||
| int $code = 0, | ||
| ?\Throwable $previous = null, | ||
| ) { | ||
| parent::__construct($message, $code, $previous); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.