Migrations - phax/ph-schematron GitHub Wiki

This page lists known traps and pit falls when migrating from one version to another. It is highly recommended to only perform a step of one major version, and not skip a major version.

v10.0 → v10.1

v10.1 is a security hardening release. There are no renames and no moved classes - everything that breaks does so because a resource that used to be fetched is now refused. If all your Schematron files, their includes and the documents they reference live on the class path or in the file system, nothing changes for you.

At a glance

Area Change Breaking? What to do
Remote document() / doc() / unparsed-text() Denied in all engines (the pure engines had no restriction at all before) Yes, if used Allow the scheme explicitly, see below
xsl:import / xsl:include from a remote URL Denied (previously fetched by Saxon despite the log warning) Yes, if used Copy the module locally, or allow the scheme
xsl:result-document Rejected at compile time (XTSE0010) in a custom Schematron / XSLT Yes, if used See below
system-property('x') (no namespace) Returns "" instead of the JVM system property Behavioural Pass the value as a Schematron parameter instead
environment-variable() Returns the empty sequence Behavioural Pass the value as a Schematron parameter instead
Custom URIResolver No longer consulted for a remote href Yes, if used Allow the scheme on the resolver you pass in
XPathConfigBuilder.DEFAULT_PROCESSOR Now a secured Processor, shared with the pure XSLT engine Behavioural Pass your own Processor to opt out
SchematronPureXsltCache Cache key no longer contains the Processor; a non-default Processor bypasses the cache Behavioural Nothing - the cache now actually hits. Set forceCacheResult (true) to cache with your own Processor
Extension functions Registered ones still work (Saxon trusts them) No -
ph-commons 12.4.0 → 12.5.0 Maven only -

Allowing a remote scheme again

For the XSLT based engines (SchematronResourceXSLT, SchematronResourceSCH, SchematronResourceSchXslt_XSLT2, SchematronResourceSchXslt2) the block sits in the ph-commons URIResolver, so hand in one that permits the scheme:

final SchematronResourceSCH aSCH = SchematronResourceSCH.builder (aRes)
                                                        .uriResolver (new DefaultTransformURIResolver ().setAllowedRemoteSchemes ("http",
                                                                                                                                 "https"))
                                                        .build ();

For the pure engines (SchematronResourcePureXPath, SchematronResourcePureXslt) the block sits in the Saxon Processor, so set the allowed schemes before the first validation:

SchematronProcessorFactory.setAllowedRemoteSchemes ("http", "https");

Both are global-ish switches on purpose: the Processor objects and the compiled artefacts derived from them are cached, so a later change has no effect on what was already created.

Re-enabling external functions

Disabling external functions is what secure processing means for Saxon; it also disables xsl:result-document, the no-namespace system-property() and environment-variable(). To get the old behaviour back:

// XSLT based engines
SchematronTransformerFactory.setTransformerFactoryCustomizer (aTF -> aTF.setAttribute (FeatureKeys.ALLOW_EXTERNAL_FUNCTIONS,
                                                                                       Boolean.TRUE));
// pure engines
SchematronProcessorFactory.setAllowExternalFunctions (true);

Prefer passing data in as a Schematron parameter over re-enabling this.

v9.2 → v10.0

v10 is a major restructuring release. Most changes are backward compatible (old names kept as deprecated aliases, old configuration strings still accepted), but there are a handful of genuinely breaking changes — concentrated in the SVRL JAXB model, the PS data model, and the ESchematronEngine enum constants. Read the "At a glance" table first, then the section relevant to how you use the library.

Nothing about the validation results changes for a typical consumer that only calls getSchematronValidity(...) / applySchematronValidationToSVRL(...) on a pre-built SchematronResource*. The breaking items only bite if you (a) parse the SVRL JAXB model yourself, (b) walk/edit the PSSchema model, (c) reference ESchematronMode / ESchematronEngine.PURE in Java, or (d) depend on the moved Maven artifacts / renamed packages.

At a glance

Area Change Breaking? What to do
Modules New ph-schematron-isosch, ph-schematron-model, ph-schematron-pure-xslt Maven only Add the new dependency if you used the moved classes
Packages Model packages drop the .pure. segment Source Update imports (sed snippet below)
Engine enum ESchematronMode deprecated; ESchematronEngine.PURE renamed to PURE_XPATH Source (Java) Use ESchematronEngine; rename PURE→PURE_XPATH
Entry point SchematronResourcePure → SchematronResourcePureXPath Optional Old name works (deprecated); adopt when convenient
New engine SchematronResourcePureXslt (pure-Java XSLT 3.0) Additive Opt-in
Builders New fluent builder() / buildCached() / buildUncached() API Additive Old from*(...) factories + some setters deprecated
Customizers / caches TransformerCustomizerSCH & siblings, EStep* enums and the static SchematronResource*Cache classes deleted Yes See Removed customizer and static cache classes below
SVRL model JAXB iterator renamed; getFlag() → List; typed content getters; elements removed Yes See SVRL below
PS model getAllPatterns() no longer returns <group>s; PSPattern/PSGroup are siblings Yes See PS model below
ESchematronVersion.LATEST Now resolves to SCHEMATRON_2025 (was 2020) Behavioural Pin an explicit version if you relied on 2020
Telemetry Optional OpenTelemetry spans/metrics Additive Opt-in
Maven plugin / Ant validate goal now supports SchXslt & pure-xslt Additive Config strings unchanged

Dependencies and build

  • ph-commons 12.2.6 → 12.3.2.
  • Saxon-HE stays at 12.9.
  • Minimum Java stays at 17 (unchanged since v9.0.0).
  • New transitive dependency ph-telemetry (com.helger.telemetry:ph-telemetry, currently 1.0.1) is pulled in by the pure engines. It is a no-op unless you register a telemetry SPI — see Telemetry.

New reactor order

ph-schematron-testfiles
ph-schematron-api
ph-schematron-xslt
ph-schematron-isosch      (new)
ph-schematron-schxslt
ph-schematron-schxslt2
ph-schematron-model       (new)
ph-schematron-pure-xpath  (folder renamed; artifactId stays ph-schematron-pure)
ph-schematron-pure-xslt   (new)
ph-schematron-validator
ph-schematron-maven-plugin
ph-schematron-ant-task
ph-schematron-benchmarks  (new, build-internal JMH benchmarks — not a consumer dependency)
ph-schematron-it          (new, build-internal integration tests — not a consumer dependency)

Module restructuring

New module: ph-schematron-isosch

The "SCH → XSLT preprocessing" half of ph-schematron-xslt (the ISO Schematron stylesheet chain iso_dsdl_include.xsl → iso_abstract_expand.xsl → iso_svrl_for_xslt2.xsl) was extracted into a new module ph-schematron-isosch. ph-schematron-xslt keeps only the "apply a pre-built XSLT to an XML instance" side.

What v9.2 location v10 location
com.helger.schematron.sch.SchematronResourceSCH ph-schematron-xslt ph-schematron-isosch
com.helger.schematron.sch.SchematronProviderXSLTFromSCH ph-schematron-xslt ph-schematron-isosch
com.helger.schematron.sch.SchematronResourceSCHCache ph-schematron-xslt deleted — use com.helger.schematron.sch.SchematronSCHCache (ph-schematron-isosch)
com.helger.schematron.sch.TransformerCustomizerSCH ph-schematron-xslt deleted — use com.helger.schematron.sch.SchematronSCHConfig (ph-schematron-isosch)
com.helger.schematron.sch.EStepSCH ph-schematron-xslt deleted — engine-internal, no public replacement
external/schematron/20100710-xslt2/*.xsl ph-schematron-xslt ph-schematron-isosch
com.helger.schematron.xslt.SchematronResourceXSLT ph-schematron-xslt ph-schematron-xslt (unchanged)
com.helger.schematron.xslt.SchematronProviderXSLTPrebuild ph-schematron-xslt ph-schematron-xslt (unchanged)
com.helger.schematron.xslt.SchematronResourceXSLTCache ph-schematron-xslt deleted — use com.helger.schematron.xslt.SchematronXSLTCache (ph-schematron-xslt)

For everything that survived, the package names are not changed — only the Maven artifact moves. The four deleted rows are the exception; see Removed customizer and static cache classes for the replacement code. If your code uses SchematronResourceSCH, add the new dependency:

<dependency>
  <groupId>com.helger.schematron</groupId>
  <artifactId>ph-schematron-isosch</artifactId>
  <version>10.0.0</version>
</dependency>

If you only use SchematronResourceXSLT (apply a pre-built XSLT), you continue depending on ph-schematron-xslt and need nothing new.

New module: ph-schematron-model

The engine-agnostic data model, SCH XML reader/writer, error handler types and the preprocessor were extracted from ph-schematron-pure into a new module ph-schematron-model. The bound-schema, binding, XPath and validation layers stay in ph-schematron-pure. The packages were also renamed to drop the .pure. segment (it no longer reflects the artifact they live in).

v9.2 package v10 package New artifact
com.helger.schematron.pure.model com.helger.schematron.model ph-schematron-model
com.helger.schematron.pure.exchange com.helger.schematron.exchange ph-schematron-model
com.helger.schematron.pure.errorhandler com.helger.schematron.errorhandler ph-schematron-model
com.helger.schematron.pure.preprocess com.helger.schematron.preprocess ph-schematron-model

To enable the preprocessor to live in ph-schematron-model without dragging the binding/bound layers along, the string-level transformation methods of IPSQueryBinding (getNegatedTestExpression, getStringReplacementMap, getWithParamTextsReplaced) were promoted to a new engine-agnostic super-interface com.helger.schematron.model.IPSQueryBindingTransform. IPSQueryBinding in ph-schematron-pure now extends IPSQueryBindingTransform. PSPreprocessor's constructor parameter, field type and getQueryBinding() return type are now IPSQueryBindingTransform. Existing callers passing an IPSQueryBinding instance keep working unchanged (Liskov substitution); only callers that explicitly typed a variable as the result of getQueryBinding() may need to widen back via cast or update the variable type.

If your code references any of the renamed packages, update the imports. A single sed pass works for typical code bases:

find . -name '*.java' -type f -print0 | xargs -0 sed -i.bak \
  -e 's|com\.helger\.schematron\.pure\.model\.|com.helger.schematron.model.|g' \
  -e 's|com\.helger\.schematron\.pure\.exchange\.|com.helger.schematron.exchange.|g' \
  -e 's|com\.helger\.schematron\.pure\.errorhandler\.|com.helger.schematron.errorhandler.|g' \
  -e 's|com\.helger\.schematron\.pure\.preprocess\.|com.helger.schematron.preprocess.|g'

Consumers that depend on ph-schematron-pure get the new module transitively and do not need a new explicit Maven dependency. Consumers that only need to read, write, preprocess or hold the Schematron data model without validating may depend on ph-schematron-model directly — it is much smaller than ph-schematron-pure (no Saxon, no binding, no XPath evaluation).

Pure engines renamed: pure-xpath / pure-xslt

The two pure-Java engines are now consistently named pure-xpath (the XPath-only engine, previously just "pure") and pure-xslt (the new in-v10 Saxon-native engine that emits XSLT 3.0 in Java and runs it through Saxon s9api).

v9.2 / pre-rename v10.0 canonical
Class com.helger.schematron.pure.SchematronResourcePure Kept, now @Deprecated(since="10.0.0", forRemoval=false). New canonical name com.helger.schematron.pure.SchematronResourcePureXPath — a thin source-compatible subclass with covariant factory return types. Existing code keeps compiling; new code should use the new name.
Module folder ph-schematron-pure Renamed on disk to ph-schematron-pure-xpath/. The artifactId stays ph-schematron-pure for source compatibility — your <dependency> is unchanged.
Class com.helger.schematron.puresaxon.SchematronResourceSaxon (interim v10 name) Renamed to com.helger.schematron.purexslt.SchematronResourcePureXslt (note the casing: PureXslt, not PureXSLT). Module folder ph-schematron-pure-xslt/, artifactId ph-schematron-pure-xslt. No alias kept — the module was new in v10, no v9 consumers exist.

SchematronResourcePure → SchematronResourcePureXPath is optional: the old name continues to work, just emits a deprecation warning at the type declaration site. Adopt the new name when convenient.

New engine: ph-schematron-pure-xslt

A brand-new pure-Java engine that generates an XSLT stylesheet from the parsed PSSchema (no external ISO Schematron stylesheet chain) and runs it through Saxon s9api. Entry point com.helger.schematron.purexslt.SchematronResourcePureXslt accepts a URIResolver, ErrorListener and a configurable XSLT version (EPureXsltVersion — XSLT 1.0 / 2.0 / 3.0, default 3.0; the generated stylesheet's SVRL @location computation adapts to stay conformant to the selected version), and supports <xsl:*> pass-through (xsl:function, xsl:key, xsl:include, …) from foreign children of <sch:schema> as well as <xsl:choose>-bodied <sch:let>.

It also ships com.helger.schematron.purexslt.xslt.SchematronToXsltConverter — a stand-alone SCH → XSLT tool that emits to a W3C DOM Document / String / OutputStream / Writer / File. To avoid clashing with the ISO-XSLT engine's similarly named types, this engine's classes use the PureXslt* prefix (PureXsltStylesheetGenerator, PureXsltQueryBindingTransform, PureXsltTelemetry, EPureXsltVersion).

<dependency>
  <groupId>com.helger.schematron</groupId>
  <artifactId>ph-schematron-pure-xslt</artifactId>
  <version>10.0.0</version>
</dependency>

Engine selection: ESchematronMode → ESchematronEngine

ESchematronMode (in ph-schematron-api, package com.helger.schematron) is now @Deprecated(since="10.0.0", forRemoval=true). It has been merged into ESchematronEngine, which is the single authoritative engine selector. ESchematronMode still exists and still works; every value carries a bridge method toEngine(), and every string id it recognised is still accepted by ESchematronEngine.getFromIDOrNull(String).

Two changes are source-breaking for Java code that names enum constants:

  1. ESchematronEngine.PURE was renamed to ESchematronEngine.PURE_XPATH. A deprecated compatibility field public static final ESchematronEngine PURE = PURE_XPATH; is kept, so ESchematronEngine.PURE still compiles — but you should migrate to PURE_XPATH.
  2. New constant ESchematronEngine.XSLT_PREBUILT (the former ESchematronMode.XSLT, id "xslt"), plus the new PURE_XSLT engine.

ESchematronEngine constants and accepted string ids (all pre-v10 ids still resolve):

Constant Canonical id Accepted aliases
PURE_XPATH pure-xpath pure
PURE_XSLT pure-xslt pure-saxon
ISO_SCHEMATRON iso-schematron iso, isoschematron, schematron, sch
SCHXSLT1 schxslt schxslt1, schxslt-xslt2
SCHXSLT2 schxslt2 —
XSLT_PREBUILT xslt —

ESchematronMode → ESchematronEngine value mapping:

ESchematronMode (v9) ESchematronEngine (v10)
PURE / PURE_XPATH PURE_XPATH
PURE_XSLT PURE_XSLT
SCHEMATRON ISO_SCHEMATRON
SCHXSLT_XSLT2 SCHXSLT1
XSLT XSLT_PREBUILT

Developer action: replace ESchematronMode with ESchematronEngine (or call .toEngine()); rename the constants PURE→PURE_XPATH and mode-XSLT→XSLT_PREBUILT. String-based configuration (e.g. schematronProcessingEngine="schematron") needs no change.


New fluent builder API

Every SchematronResource* class gained a nested Builder plus static factory methods. This is additive — existing single-argument constructors (e.g. new SchematronResourceSCH(IReadableResource)) are not deprecated.

New per-class API (illustrated with SchematronResourceSCH; the same shape exists on SchematronResourcePureXPath, SchematronResourcePureXslt, SchematronResourceSchXslt_XSLT2, SchematronResourceSchXslt2, SchematronResourceXSLT):

  • Static factories returning a Builder: builder(IReadableResource), builderFromClassPath(...), builderFromFile(...), builderFromURL(...), builderFromInputStream(...), builderFromByteArray(...), builderFromString(...).
  • Fluent setters on Builder: useCache, lenient, entityResolver, phase, languageCode, errorListener, uriResolver, parameter / parameters, forceCacheResult, transformerFactoryCustomizer, telemetry(...), perAssertionResultTelemetry, perRuleExecutionTelemetry, outputValidityDeterminator, validateSVRL, cache.
  • Terminal methods:
    • build() — lazy compile (existing semantics).
    • buildCached() / buildCached(cache) — eagerly compile through the cache.
    • buildUncached() — eagerly compile once, bypassing the cache (fail-fast).
  • One-shot convenience returning the resource: compileCached(...), compileUncached(...).
final SchematronResourceSCH aSCH = SchematronResourceSCH.builderFromFile ("rules.sch")
    .phase ("full")
    .languageCode ("en")
    // compiles immediately, throws on error
    .buildUncached ();

The following are now @Deprecated(since="10.0.0", forRemoval=false) in favour of the builder (they keep working):

  • The old static factories fromClassPath(...), fromFile(...), fromURL(...), fromInputStream(...), fromByteArray(...), fromString(...) → use builderFrom*(...).
  • The mutating setters setPhase(String), setLanguageCode(String), setForceCacheResult(boolean) → configure via the Builder.

Cache SPI (advanced)

A generic compilation-cache abstraction was published in ph-schematron-api, package com.helger.schematron.api.cache: ISchematronCompilationCacheKey (marker), ISchematronCompilation<ARTIFACT> (a single compilation step: getResource(), getCacheKey(), canCacheResult(), compile()), and AbstractSchematronCache<CFG, ARTIFACT>. Each engine now exposes a value/compilation class via resource.toConfig() (SchematronSCHConfig, SchematronPureXPathConfig, SchematronPureXsltConfig, SchematronSchXslt_XSLT2Config, SchematronSchXslt2Config, SchematronXSLTConfig). Ordinary callers are unaffected; this only matters if you implement a custom engine cache or drive compilation manually. There is no new public "validator" interface to implement (an ISchematronValidator family was prototyped mid-cycle but removed before release). The v9 classes that this SPI replaces were deleted, not deprecated — see the next section.


Removed customizer and static cache classes

The TransformerCustomizer* mutable value holders, the EStep* enums and the static SchematronResource*Cache utility classes no longer exist in v10.0.0. They are not deprecated aliases — the classes are gone, so code referencing them fails to compile.

Their responsibilities were split into the two new per-engine types:

  • Schematron*Config — the immutable "what to compile and how" value object (phase, language code, error listener, URI resolver, XSLT parameters, cache-forcing, telemetry, transformer-factory customizer). This replaces TransformerCustomizer*.
  • Schematron*Cache — the instance-based artifact cache. This replaces the static SchematronResource*Cache.
v9.2 class (gone in v10) v10 replacement
com.helger.schematron.sch.TransformerCustomizerSCH com.helger.schematron.sch.SchematronSCHConfig
com.helger.schematron.sch.SchematronResourceSCHCache com.helger.schematron.sch.SchematronSCHCache
com.helger.schematron.sch.EStepSCH — engine-internal now, no public replacement
com.helger.schematron.schxslt.xslt2.TransformerCustomizerSchXslt_XSLT2 com.helger.schematron.schxslt.xslt2.SchematronSchXslt_XSLT2Config
com.helger.schematron.schxslt.xslt2.SchematronResourceSchXslt_XSLT2Cache com.helger.schematron.schxslt.xslt2.SchematronSchXslt_XSLT2Cache
com.helger.schematron.schxslt.xslt2.EStepSchXslt_XSLT2 — engine-internal now, no public replacement
com.helger.schematron.schxslt2.xslt.TransformerCustomizerSchXslt2 com.helger.schematron.schxslt2.xslt.SchematronSchXslt2Config
com.helger.schematron.schxslt2.xslt.SchematronResourceSchXslt2Cache com.helger.schematron.schxslt2.xslt.SchematronSchXslt2Cache
com.helger.schematron.xslt.SchematronResourceXSLTCache com.helger.schematron.xslt.SchematronXSLTCache (+ SchematronXSLTConfig)

Replacing TransformerCustomizerSCH

Every setter has a 1:1 counterpart on SchematronSCHConfig.Builder — and the identical method names also exist on SchematronResourceSCH.Builder, so most code never has to name the config class at all.

TransformerCustomizerSCH (v9.2) SchematronSCHConfig.Builder / SchematronResourceSCH.Builder (v10)
new TransformerCustomizerSCH () SchematronSCHConfig.builder (aRes) — or SchematronResourceSCH.builder (aRes)
setErrorListener (ErrorListener) errorListener (ErrorListener)
setURIResolver (URIResolver) uriResolver (URIResolver)
setParameters (Map <String, ?>) parameters (Map <String, ?>) — or parameter (String, Object) one at a time
setPhase (String) phase (String)
setLanguageCode (String) languageCode (String)
setForceCacheResult (boolean) forceCacheResult (boolean)
overriding customize (TransformerFactory) transformerFactoryCustomizer (Consumer <TransformerFactory>)
overriding customize (EStepSCH, Transformer) — no replacement; the engine applies the config itself

The read side (getPhase(), getLanguageCode(), getErrorListener(), getURIResolver(), getParameters(), hasParameters(), isForceCacheResult(), canCacheResult()) exists under the same names on the immutable SchematronSCHConfig.

Case 1 — you only configured the resource. Nothing to migrate beyond the method names:

// v9.2
final SchematronResourceSCH aSCH = SchematronResourceSCH.fromFile ("rules.sch");
aSCH.setPhase ("full");
aSCH.setLanguageCode ("en");
aSCH.setForceCacheResult (true);

// v10
final SchematronResourceSCH aSCH = SchematronResourceSCH.builderFromFile ("rules.sch")
    .phase ("full")
    .languageCode ("en")
    .forceCacheResult (true)
    .build ();

Case 2 — you subclassed to register Saxon extension functions. In v9 this meant overriding the @OverrideOnDemand hook SchematronResourceSCH.createTransformerCustomizer(), returning a TransformerCustomizerSCH subclass that overrode customize (TransformerFactory) — and then manually re-applying all default values. Both the hook and the helper applyDefaultValuesOnTransformerCustomizer(...) are gone. This is now a first-class, subclass-free builder option:

// v9.2 — see the old ph-schematron issue #129 example
final SchematronResourceSCH aSCH = new SchematronResourceSCH (new FileSystemResource (aSchematron))
{
  @Override
  @NonNull
  @OverrideOnDemand
  protected TransformerCustomizerSCH createTransformerCustomizer ()
  {
    final TransformerCustomizerSCH aCustomizer = new TransformerCustomizerSCH ()
    {
      @Override
      public void customize (@NonNull final TransformerFactory aTransformerFactory)
      {
        super.customize (aTransformerFactory);
        if (aTransformerFactory instanceof TransformerFactoryImpl)
        {
          final TransformerFactoryImpl aSaxonTF = (TransformerFactoryImpl) aTransformerFactory;
          final Configuration aSaxonConfig = aSaxonTF.getConfiguration ();
          final Processor aProcessor = (Processor) aSaxonConfig.getProcessor ();
          aProcessor.registerExtensionFunction (new EF_Test ());
        }
      }
    };
    return aCustomizer.setErrorListener (getErrorListener ())
                      .setURIResolver (getURIResolver ())
                      .setParameters (parameters ())
                      .setPhase (getPhase ())
                      .setLanguageCode (getLanguageCode ())
                      .setForceCacheResult (isForceCacheResult ());
  }
};
// v10 — no subclass, no re-applying of defaults
final SchematronResourceSCH aSCH = SchematronResourceSCH.builder (new FileSystemResource (aSchematron))
    .transformerFactoryCustomizer (aTransformerFactory -> {
        if (aTransformerFactory instanceof final TransformerFactoryImpl aSaxonTF)
        {
          final Configuration aSaxonConfig = aSaxonTF.getConfiguration ();
          final Processor aProcessor = (Processor) aSaxonConfig.getProcessor ();
          aProcessor.registerExtensionFunction (new EF_Test ());
        }
    })
    .build ();

Notes on transformerFactoryCustomizer (...):

  • It receives the TransformerFactory of the final compile step (the one that compiles the validation stylesheet), after the error listener and URI resolver were applied and immediately before compilation — the same point at which customize (TransformerFactory) used to be called.
  • Because the cache key cannot capture the identity of an arbitrary lambda, setting a customizer makes canCacheResult() return false, i.e. it disables caching — unless you also set forceCacheResult (true).
  • The same option exists on SchematronResourceXSLT, SchematronResourceSchXslt_XSLT2, SchematronResourceSchXslt2 and on all four Schematron*Config.Builder classes.

Case 3 — you called the static cache directly.

v9.2 v10
SchematronResourceSCHCache.getSchematronXSLTProvider (aRes, aTC) SchematronSCHCache.shared ().getOrCompile (aConfig)
SchematronResourceSCHCache.createSchematronXSLTProvider (aRes, aTC) aConfig.compile ()
SchematronResourceSCHCache.clearCache () SchematronSCHCache.shared ().clear ()
// v10
final SchematronSCHConfig aConfig = SchematronSCHConfig.fromFile ("rules.sch")
    .phase ("full")
    .build ();

// Cached; automatically falls back to a plain compile when aConfig.canCacheResult () is false
final ISchematronXSLTBasedProvider aProvider = SchematronSCHCache.shared ().getOrCompile (aConfig);

Besides shared (), SchematronSCHCache can be instantiated per use case — optionally with a name and a maximum size (new SchematronSCHCache ("my-cache", 100)) — and handed to a resource via SchematronResourceSCH.builder (...).cache (aMyCache). It also offers isCached(cfg), invalidate(cfg), size() and clear(). The static v9 cache offered none of this.

The SchematronProviderXSLTFromSCH constructor is no longer public either — it now takes a SchematronSCHConfig and is protected. Go through SchematronSCHConfig.compile () (uncached) or SchematronSCHCache.getOrCompile (...) (cached) instead.

The other engines

The same mechanical rename applies to SchXslt 1.x, SchXslt 2.x and the pre-built XSLT engine. The builder method names (errorListener, uriResolver, parameter / parameters, phase, languageCode, forceCacheResult, telemetry, transformerFactoryCustomizer) are identical across SchematronSCHConfig, SchematronSchXslt_XSLT2Config and SchematronSchXslt2Config. SchematronXSLTConfig (pre-built XSLT) has no phase / languageCode / forceCacheResult, because there is no SCH preprocessing step to parameterize.


SVRL changes

This is the area most likely to break a consumer that inspects validation output. Two independent things changed: the emitted SVRL XML was made conformant with the official SVRL / SVRL 2025, and the generated JAXB model changed shape accordingly.

Emitted SVRL XML (what the engines produce)

The SVRL namespace is unchanged (http://purl.oclc.org/dsdl/svrl). Output differences:

  • <svrl:text> is now emitted last inside failed-assert / successful-report (after diagnostic-reference / property-reference), instead of first. Do not rely on text being the first child.
  • Rich attributes (see, icon, fpi, xml:lang, xml:space) moved off the message element and into the <svrl:text> element for failed-assert, successful-report and diagnostic-reference.
  • fired-rule no longer carries rich attributes — only context, id, role and properties remain.
  • diagnostic-reference content is now wrapped in <svrl:text> rather than being applied directly.
  • active-pattern/@document is gone; only @documents is emitted (they were emitted in parallel since v8.0.4; the singular form is now dropped).
  • An empty flag attribute is no longer written — flag appears only when non-empty.
  • schematron-output/@title and @schemaVersion are still emitted by the XSLT engine.

If you have code (or XSLT/XPath, or golden-file tests) that reads SVRL by attribute position or expects rich attributes on the assert/report element, update it.

SVRL XSD (svrl.xsd)

  • flag: xs:NMTOKEN → list of xs:token (drives the JAXB getFlag() type change below).
  • New severity attribute (xs:string) on assert/report.
  • role: xs:NMTOKEN → xs:string; phase: xs:NMTOKEN → xs:string (loosened; Java type stays String).
  • New active-group element (alongside active-pattern).
  • failed-assert / successful-report content model changed from a mixed <xs:choice> to a typed <xs:sequence> (diagnostic-reference*, property-reference*, text).
  • Removed elements: message-code, message-category, suppressed-rule (these SchXslt extension elements added in v8.0.6 are gone from the model; unknown foreign-namespace content is still accepted via xs:any on schematron-output).

SVRL JAXB model (com.helger.schematron.svrl.jaxb) — breaking

The package and target namespace are unchanged, but generated method names changed. If you walk or build the SVRL model directly:

v9 v10
SchematronOutputType.getActivePatternAndFiredRuleAndFailedAssert() getActivePatternOrActiveGroupAndFiredRule() (and matching addActivePatternOrActiveGroupAndFiredRule(...))
FailedAssert.getFlag() / SuccessfulReport.getFlag() → String List<String> (new helpers hasFlagEntries(), getFlagAtIndex(int), addFlag(...))
— new getSeverity() on FailedAssert / SuccessfulReport
getDiagnosticReferenceOrPropertyReferenceOrText() → List<Object> split into typed getDiagnosticReference() (List<DiagnosticReference>), getPropertyReference() (List<PropertyReference>), getText() (single Text)
ActivePattern only new concrete classes ActivePattern and ActiveGroup

SVRL helper API (com.helger.schematron.svrl)

  • Public method signatures on SVRLHelper, SVRLFailedAssert, SVRLSuccessfulReport are unchanged (only their bodies were updated to use the new typed getters).
  • Six SVRLHelper methods are now @Deprecated(since="10.0.0", forRemoval=true) because the JAXB model now exposes typed getters directly: getAllDiagnosticReferences(FailedAssert), getAllPropertyReferences(FailedAssert), getText(FailedAssert) and the three SuccessfulReport equivalents. Migrate to the model getters (aFA.getDiagnosticReference(), aFA.getPropertyReference(), aFA.getText()).
  • ISVRLErrorLevelDeterminator precedence changed. The default resolution order for the error level of a failed-assert / successful-report is now severity → first flag entry → role (was flag → role). If you supply a custom ISVRLErrorLevelDeterminator, mirror the new precedence and read flag from the list (e.g. hasFlagEntries() ? getFlagAtIndex(0) : null).

PS model changes

Beyond the package move (above), the PS data model gained ISO/IEC 19757-3 edition awareness (2006 / 2016 / 2020 / 2025) and grew new element types. Most of this is additive, but three items are breaking or behavioural for code that walks or edits PSSchema.

<group> support — getAllPatterns() no longer sees everything (breaking, silent)

2025 introduces the <group> container. A new abstract base com.helger.schematron.model.AbstractPSPatternLike was extracted; PSPattern and PSGroup are now siblings, both extending it. Consequences:

  • PSSchema.getAllPatterns() returns patterns only. Groups live in a separate collection reached via the new getAllGroups() (with getGroupCount(), getGroupOfID(...), addGroup(...), hasAnyGroup()). Code that iterated getAllPatterns() to enumerate all rule-bearing containers will now silently skip every <group>.
  • instanceof PSPattern no longer matches a group (by design). If you mean "pattern or group", test instanceof AbstractPSPatternLike.
  • A schema can now be valid with zero <pattern>s if it has <group>s, so getPatternCount() == 0 no longer implies an empty/invalid schema. PSSchema.isValid() fails only when both patterns and groups are empty.
  • Familiar methods (getAllRules(), addRule(), isAbstract(), getAllLets(), …) now live on AbstractPSPatternLike rather than being declared on PSPattern. Source compiles unchanged, but pre-compiled bytecode that referenced PSPattern.<method> must be recompiled (binary-incompatible).

ESchematronVersion.LATEST repointed (behavioural)

ESchematronVersion (in ph-schematron-api, package com.helger.schematron) gained SCHEMATRON_2025, and **LATEST now resolves to SCHEMATRON_2025 instead of SCHEMATRON_2020. ** Any code reading ESchematronVersion.LATEST now gets 2025 — pin an explicit value if you depended on 2020. New accessors: getEditionYear() ("2025", …), static getFromEditionYearOrNull(String), and isOlderThan(ESchematronVersion).

PSVersionChecker warnings on read and write (behavioural)

A new PSVersionChecker runs automatically at the end of every PSReader.readSchemaFromXML(...) and at the start of every PSWriter write (writeToFile/writeToStream/writeToWriter/getAsString). It emits warnings only (never errors, never exceptions; return values unchanged) through the registered error handler when a schema uses a feature newer than its declared schematronEdition — or when no edition is declared at all. With the default LoggingPSErrorHandler these appear as logged WARN messages.

  • Impact: code that asserts "no error-handler callbacks / no warnings" during read or write may now trip. Supply your own IPSErrorHandler (PSReader) / PSWriter.setErrorHandler(...) to redirect or suppress.
  • Typical pre-2025 schemas that don't use new features stay silent. Note queryBinding is the one check that only warns when an explicit older edition is declared, so a common queryBinding="xslt2" schema with no schematronEdition produces no warning.

Validation actually relaxed (not tightened)

The PSPattern/PSGroup validity checks were realigned to the 2025 (v4) RNC three-branch shape. Net effect: two combinations that were hard errors in v9 are now allowed — an abstract pattern with is-a, and an abstract pattern with <param> (the latter flagged as a PSVersionChecker warning on pre-2025 schemas, not a hard error). Nothing that previously validated now fails, and the same rules now apply to <group>.

New model classes & attributes (additive)

  • New classes in com.helger.schematron.model: PSGroup, PSLibrary (a <library> root document; read via PSReader.readLibrary() / readLibraryFromXML(...)), PSRules, PSProperties, PSProperty, and the base AbstractPSPatternLike.
  • PSSchema.getSchematronEdition() / setSchematronEdition(ESchematronVersion) carry the 2025 schematronEdition attribute.
  • New attribute accessors (all additive): PSLet.getAs()/setAs(...); PSPhase.getFrom()/getWhen(); PSRule.getVisitEach(), getSeverity(); PSAssertReport.getSeverity(), getAllProperties(); role/documents on both pattern and group.
  • flag is now a whitespace-separated token list. IPSHasFlag exposes getAllFlags() / addFlag(...), and setFlag(String) splits on whitespace. The old getFlag() still returns a single value (the first token), so code that must see all tokens should switch to getAllFlags().

Telemetry (opt-in)

Both SchematronResourcePureXPath and SchematronResourcePureXslt expose setTelemetry(boolean) and setPerAssertionTelemetry(boolean) (also configurable via the builder's telemetry(...) / perAssertionResultTelemetry / perRuleExecutionTelemetry). When enabled, the engines emit OpenTelemetry-shaped spans (schematron.validate, schematron.parse, schematron.preprocess, schematron.generate, schematron.compile, schematron.execute, optional schematron.assertion), counters and a schematron.validate.duration histogram. This is zero runtime cost when no ITelemetryTracerSPI / ITelemetryMeterSPI is registered — ph-telemetry degrades to a silent no-op. Nothing to do unless you want the instrumentation.


Maven plugin & Ant task

Public XML parameter names are unchanged (schematronProcessingEngine, forceCacheResult, phaseName, languageCode, …), and all pre-v10 engine string values still resolve (via ESchematronEngine aliases). The engine coverage changed:

  • validate goal (SchematronValidationMojo) now handles all six engines — pure-xpath, pure-xslt, iso-schematron, schxslt, schxslt2, xslt. In v9 the validation goal only handled pure, schematron and xslt; SchXslt (v1 and v2) and pure-xslt are newly usable here. phaseName, languageCode and parameters apply.
  • convert goal (Schematron2XSLTMojo) additionally accepts pure-xslt (because PURE_XSLT is XSLT-based). Supported convert engines: iso-schematron, schxslt, schxslt2, pure-xslt (pure-xpath is still rejected, as it produces no XSLT).
  • Ant task Schematron now handles both pure-xslt and schxslt2 too. All six engine values (pure-xpath, pure-xslt, iso-schematron, schxslt, schxslt2, xslt) are supported, matching the validate goal.

v9.1 → v9.2

v9.2 only affects ph-schematron-pure. All other modules (ph-schematron-api, ph-schematron-xslt, ph-schematron-schxslt, ph-schematron-schxslt2, ph-schematron-maven-plugin, ph-schematron-ant-task, ph-schematron-validator) are source-compatible.

What changes

ph-schematron-pure was migrated from the JAXP XPath API (javax.xml.xpath.*, internally limited to XPath 1.0) to the Saxon s9api (net.sf.saxon.s9api.*). Schematron expressions are now compiled and evaluated as XPath 3.1 by default.

The user-facing entry points keep their DOM signatures:

  • SchematronResourcePure.applySchematronValidationToSVRL(org.w3c.dom.Node, String) — unchanged.
  • SchematronResourcePure.getSchematronValidity(org.w3c.dom.Node, String) — unchanged.
  • IPSValidationHandler — still receives org.w3c.dom.Node / NodeList. Existing implementations of this interface require no changes.
  • IPSErrorHandler — unchanged.

If you only call into these entry points, you do not need any source change. You may want to review your Schematrons for XPath 1.0 quirks though — see "Behavioural change" below.

XPath configuration (IXPathConfig / XPathConfigBuilder)

All JAXP-typed methods on XPathConfigBuilder were removed:

Old (9.1) New (9.2)
setXPathFactory(javax.xml.xpath.XPathFactory) setProcessor(net.sf.saxon.s9api.Processor)
setXPathFactoryClass(...) setProcessor(new Processor(false)) etc.
setGlobalXPathFactory(String) (removed — no longer relevant)
setXPathVariableResolver(javax.xml.xpath.XPathVariableResolver) addExternalVariable(QName, XdmValue) / addAllExternalVariables(Map)
setXPathFunctionResolver(javax.xml.xpath.XPathFunctionResolver) addExtensionFunction(net.sf.saxon.s9api.ExtensionFunction) / addAllExtensionFunctions(Iterable)

The XPath language version is now an enum, EXPathVersion, with values XPATH_1_0, XPATH_2_0, XPATH_3_0, XPATH_3_1 (default), XPATH_4_0. Set it via XPathConfigBuilder.setXPathVersion(EXPathVersion); it ends up on XPathCompiler.setLanguageVersion(String).

Custom XPath functions

JAXP XPathFunction / MapBasedXPathFunctionResolver (from ph-commons) are no longer wired into the engine. Implement net.sf.saxon.s9api.ExtensionFunction instead. Arguments arrive as XdmValue[] (instead of List<Object> of DOM Nodes), and the result is an XdmValue.

Example (old, 9.1):

final MapBasedXPathFunctionResolver fns = new MapBasedXPathFunctionResolver ();
fns.addUniqueFunction ("http://example/ns", "my-count", 1, args -> {
    final List<?> arg = (List<?>) args.get (0);
    return Integer.valueOf (arg.size ());
});
final IXPathConfig cfg = new XPathConfigBuilder ().setXPathFunctionResolver (fns).build ();

Example (new, 9.2):

final IXPathConfig cfg = new XPathConfigBuilder ()
    .addExtensionFunction (new ExtensionFunction () {
        public QName getName ()                   { return new QName ("http://example/ns", "my-count"); }
        public SequenceType [] getArgumentTypes () { return new SequenceType [] { SequenceType.ANY }; }
        public SequenceType getResultType ()      { return ItemType.INTEGER.one (); }
        public XdmValue call (final XdmValue [] a) { return new XdmAtomicValue (a[0].size ()); }
    })
    .build ();

External variables

JAXP XPathVariableResolver is gone. Bind external variables on the builder:

final IXPathConfig cfg = new XPathConfigBuilder ()
    .addExternalVariable (new QName ("my-var"), new XdmAtomicValue ("hello"))
    .build ();

XQueryAsXPathFunctionConverter

loadXQuery(InputStream) now returns ICommonsList<ExtensionFunction> (was MapBasedXPathFunctionResolver). Feed the list straight into the builder:

final IXPathConfig cfg = new XPathConfigBuilder ()
    .addAllExtensionFunctions (new XQueryAsXPathFunctionConverter ().loadXQuery (xqStream))
    .build ();

The wrapper around UserFunction now exposes the actual declared argument and result SequenceTypes, so Saxon performs automatic coercion (atomization, cardinality checks, type promotion) for free — this resolves the long-standing DOMNodeWrapper cannot be cast to AtomicValue problem when calling e.g. functx:are-distinct-values(...).

Internal API surface

If you subclass or directly consume the bound-schema internals, note:

  • PSXPathBoundSchema, PSXPathBoundRule, PSXPathBoundAssertReport, PSXPathBoundElement, PSXPathVariables, IPSXPathVariables now hold net.sf.saxon.s9api.XPathExecutable instead of javax.xml.xpath.XPathExpression.
  • XPathLetVariableResolver no longer implements javax.xml.xpath.XPathVariableResolver. It is a per-thread QName → XdmValue store used by the bound schema.
  • XPathEvaluationHelper is reshaped around XPathExecutable / XdmItem / XdmValue. The DOM-returning variants (evaluateAsNodeList, etc.) are gone; use evaluateAsXdmNodes(...) or evaluate(...).
  • A new XPathEvaluationContext (thread-local) is published. The bound schema installs one per validate(...) call; the SVRL handler uses it.

Behavioural change: XPath 1.0 → XPath 3.1

Schematrons that target XPath 2.0 / 3.0 / 3.1 in their queryBinding attribute (the common case) work as written. The engine no longer silently accepts XPath-1.0 quirks though:

  • eq / ne / lt / le / gt / ge are now value comparisons (previously syntax errors that were tolerated by some configurations).
  • Functions that don't exist in the selected XPath version fail at bind time with a clear compile error.
  • String coercion of node sequences follows XPath 2.0+ rules.

If your Schematron uses a 1.0-only construct, set XPathConfigBuilder.setXPathVersion(EXPathVersion.XPATH_1_0) to keep the legacy semantics.

Saxon TinyTree input path

SchematronResourcePure.getAsNode(...) now parses the input XML straight into a Saxon TinyTree (via Processor.newDocumentBuilder()) and presents it as a DOM facade (net.sf.saxon.dom.NodeOverNodeInfo / DocumentOverNodeInfo). All overloads of applySchematronValidationToSVRL, getSchematronValidity and applySchematronValidation that take a Source or IHasInputStream benefit automatically. Caveats:

  • If you have a custom XML EntityResolver configured on the resource, the TinyTree path is bypassed and the previous DOM parsing route is used so the resolver still works.
  • The org.w3c.dom.Node returned by getAsNode(...) in the fast path is Saxon's read-only DOM facade. Code that mutates the returned DOM will hit NOT_SUPPORTED_ERR. Read-only consumers (XPath, traversal, serialisation) are unaffected.
  • Overloads that take a pre-built org.w3c.dom.Node keep their existing behaviour (the engine wraps the DOM via Saxon's DocumentWrapper).

<let> element body

<let> elements without a value attribute now have their body text picked up as a plain XPath expression — fixes #189. Plain XPath in the body works (e.g. <let name="cnt">count(item)</let>). XSLT-instruction bodies (<xsl:for-each>, <xsl:value-of>, …) still cannot be executed by the pure engine; they now produce a clear warning naming the offending child element instead of the cryptic <let> has no 'value' failure. If you need full XSLT-body support, use SchematronResourceSCH / SchematronResourceXSLT for now.

v8 → v9

  • The minimum Java version changed from Java 11 to Java 17

v6 → v7

  • Using Java 11 as the baseline
  • Updated to ph-commons 11
  • Using JAXB 4.0 as the baseline

v5 → v6

  • The Maven group ID changed from com.helger to com.helger.schematron
  • In this version the submodule structure was modified
    • ph-schematron was split in ph-schematron-api, ph-schematron-pure and ph-schematron-xslt - you need to choose a different artifact
    • You need to pick either ph-schematron-pure or ph-schematron-xslt depending on the engine you want to use
    • Alternatively you can use the new ph-schematron-schxslt module
    • Note: the Maven Plugin and the ANT task support all engines
  • Some classes moved to different packages (e.g. SchematronResourceSCH was moved from com.helger.schematron.xslt to com.helger.schematron.sch)
⚠️ **GitHub.com Fallback** ⚠️