• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.86
/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Transform.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 */
24
package org.exist.xquery.functions.fn.transform;
25

26
import com.github.benmanes.caffeine.cache.Cache;
27
import com.github.benmanes.caffeine.cache.Caffeine;
28
import io.lacuna.bifurcan.IEntry;
29
import net.sf.saxon.expr.parser.Location;
30
import net.sf.saxon.om.StructuredQName;
31
import net.sf.saxon.s9api.*;
32
import net.sf.saxon.serialize.SerializationProperties;
33
import net.sf.saxon.trans.UncheckedXPathException;
34
import org.apache.logging.log4j.LogManager;
35
import org.apache.logging.log4j.Logger;
36
import org.exist.dom.QName;
37
import org.exist.util.Holder;
38
import org.exist.xquery.ErrorCodes;
39
import org.exist.xquery.XPathException;
40
import org.exist.xquery.XQueryContext;
41
import org.exist.xquery.functions.fn.FnTransform;
42
import org.exist.xquery.functions.map.MapType;
43
import org.exist.xquery.value.*;
44
import org.w3c.dom.Document;
45
import org.w3c.dom.Node;
46

47
import javax.annotation.Nonnull;
48
import javax.xml.transform.ErrorListener;
49
import javax.xml.transform.Source;
50
import javax.xml.transform.TransformerException;
51
import javax.xml.transform.dom.DOMSource;
52
import java.net.URI;
53
import java.time.LocalDateTime;
54
import java.util.HashMap;
55
import java.util.Map;
56
import java.util.Optional;
57

58
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
59
import static org.exist.util.StringUtil.isNullOrEmpty;
60
import static org.exist.xquery.functions.fn.transform.Options.Option.*;
61

62
/**
63
 * Implementation of fn:transform.
64
 *
65
 * This is the core of the eval() function of fn:transform
66
 * It is a separate class due to the multiplicity of options that must be dealt with,
67
 * which lead to too much code in a single file.
68
 *
69
 * This class contains the core of the logic
70
 * - create an XSLT compiler (if we don't have one compiled already, which matches our stylesheet)
71
 * - set parameters on the compiler
72
 * - create a transformer from the compiler
73
 * - set parameters on the transformer (inputs)
74
 * - invoke the transformation
75
 * - deliver and postprocess the output in the required form
76
 *
77
 * The parsing and checking of the options to fn:transform is isolated in {@link Options}
78
 * Delivery and output of the result, depending on the required format, is in {@link Delivery}
79
 *
80
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
81
 * @author <a href="mailto:alan@evolvedbinary.com">Alan Paxton</a>
82
 */
83
public class Transform {
84

85
    private static final Logger LOGGER =  LogManager.getLogger(org.exist.xquery.functions.fn.transform.Transform.class);
1✔
86
    private static final org.exist.xquery.functions.fn.transform.Transform.ErrorListenerLog4jAdapter ERROR_LISTENER = new Transform.ErrorListenerLog4jAdapter(Transform.LOGGER);
1✔
87

88
    final Convert.ToSaxon toSaxon = new Convert.ToSaxon() {
1✔
89
        @Override
90
        DocumentBuilder newDocumentBuilder() {
91
            return context.getBroker().getBrokerPool().getSaxonProcessor().newDocumentBuilder();
1✔
92
        }
93
    };
94

95
    private static final Cache<String, XsltExecutable> XSLT_EXECUTABLE_CACHE = Caffeine.newBuilder()
1✔
96
            .maximumSize(25)
1✔
97
            .weakValues()
1✔
98
            .build();
1✔
99

100
    private final XQueryContext context;
101
    private final FnTransform fnTransform;
102

103

104
    public Transform(final XQueryContext context, final FnTransform fnTransform) {
1✔
105
        this.context = context;
1✔
106
        this.fnTransform = fnTransform;
1✔
107
    }
1✔
108

109
    public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
110

111
        final Options options = new Options(context, fnTransform, toSaxon, (MapType) args[0].itemAt(0));
1✔
112

113
        //TODO(AR) Saxon recommends to use a <code>StreamSource</code> or <code>SAXSource</code> instead of DOMSource for performance
114
        final Optional<Source> sourceNode = Transform.getSourceNode(options.sourceNode, context.getBaseURI());
1✔
115

116
        if (options.xsltVersion.equals(V1_0) || options.xsltVersion.equals(V2_0) || options.xsltVersion.equals(V3_0)) {
1✔
117
            try {
118
                final Holder<XPathException> compileException = new Holder<>();
1✔
119
                final XsltExecutable xsltExecutable;
120
                if (options.shouldCache.orElse(BooleanValue.TRUE).getValue()) {
1!
121
                    xsltExecutable = Transform.XSLT_EXECUTABLE_CACHE.get(executableHash(options), key -> {
1✔
122
                        try {
123
                            return compileExecutable(options);
1✔
124
                        } catch (final XPathException e) {
1✔
125
                            compileException.value = e;
1✔
126
                            return null;
1✔
127
                        }
128
                    });
129
                } else {
1✔
130
                    xsltExecutable = compileExecutable(options);
×
131
                }
132

133
                if (compileException.value != null) {
1✔
134
                    // if we could not compile the xslt, rethrow the error
135
                    throw compileException.value;
1✔
136
                }
137
                if (xsltExecutable == null) {
1!
138
                    throw new XPathException(fnTransform, ErrorCodes.FOXT0003, "Unable to compile stylesheet (No error returned from compilation)");
×
139
                }
140

141
                final Xslt30Transformer xslt30Transformer = xsltExecutable.load30();
1✔
142

143
                options.initialMode.ifPresent(qNameValue -> xslt30Transformer.setInitialMode(Convert.ToSaxon.of(qNameValue.getQName())));
1✔
144
                xslt30Transformer.setInitialTemplateParameters(options.templateParams, false);
1✔
145
                xslt30Transformer.setInitialTemplateParameters(options.tunnelParams, true);
1✔
146
                if (options.baseOutputURI.isPresent()) {
1✔
147
                    final AtomicValue baseOutputURI = options.baseOutputURI.get();
1✔
148
                    final AtomicValue asString = baseOutputURI.convertTo(Type.STRING);
1✔
149
                    if (asString instanceof StringValue) {
1!
150
                        xslt30Transformer.setBaseOutputURI(asString.getStringValue());
1✔
151
                    }
152
                }
153

154
                // The delivery mechanism
155
                final SerializationProperties serializationProperties =
1✔
156
                        SerializationParameters.getAsSerializationProperties(
1✔
157
                                options.serializationParams.orElse(new MapType(context)),
1✔
158
                                (code, message) -> new XPathException(fnTransform, code, message));
1✔
159
                final Delivery delivery = new Delivery(context, options.deliveryFormat, serializationProperties);
1✔
160

161
                // Record the secondary result documents generated
162
                final Map<URI, Delivery> resultDocuments = new HashMap<>();
1✔
163
                xslt30Transformer.setResultDocumentHandler(resultDocumentURI -> {
1✔
164
                    final Delivery resultDelivery = new Delivery(context, options.deliveryFormat, serializationProperties);
1✔
165
                    resultDocuments.put(resultDocumentURI, resultDelivery);
1✔
166
                    return resultDelivery.createDestination(xslt30Transformer, true);
1✔
167
                });
168

169
                if (options.globalContextItem.isPresent()) {
1✔
170
                    final Item item = options.globalContextItem.get();
1✔
171
                    final XdmItem xdmItem = (XdmItem) toSaxon.of(item);
1✔
172
                    xslt30Transformer.setGlobalContextItem(xdmItem);
1✔
173
                } else if (sourceNode.isPresent()) {
1✔
174
                    final Document document;
175
                    Source source = sourceNode.get();
1✔
176
                    final Node node = ((DOMSource)sourceNode.get()).getNode();
1✔
177
                    if (!(node instanceof org.exist.dom.memtree.DocumentImpl) && !(node instanceof org.exist.dom.persistent.DocumentImpl)) {
1✔
178
                        //The source may not be a document
179
                        //If it isn't, it should be part of a document, so we build a DOMSource to use
180
                        document = node.getOwnerDocument();
1✔
181
                        source = new DOMSource(document);
1✔
182
                    }
183
                    final var brokerPool = context.getBroker().getBrokerPool();
1✔
184
                    final DocumentBuilder sourceBuilder = brokerPool.getSaxonProcessor().newDocumentBuilder();
1✔
185
                    final XdmNode xdmNode = sourceBuilder.build(source);
1✔
186
                    xslt30Transformer.setGlobalContextItem(xdmNode);
1✔
187
                } else {
1✔
188
                    xslt30Transformer.setGlobalContextItem(null);
1✔
189
                }
190

191
                final Transform.TemplateInvocation invocation = new Transform.TemplateInvocation(
1✔
192
                        options, sourceNode, delivery, xslt30Transformer, resultDocuments);
1✔
193
                return invocation.invoke();
1✔
194
            } catch (final SaxonApiException e) {
×
195
                throw originalXPathException("Could not transform input using: " + options.xsltSource._1 + ", at line: " + e.getLineNumber() + ". Error: ", e, ErrorCodes.FOXT0003);
×
196
            } catch (final  UncheckedXPathException e) {
×
197
                final Location location = e.getXPathException().getLocator();
×
198
                int line = -1;
×
199
                int column = -1;
×
200
                if (location != null) {
×
201
                    line = location.getLineNumber();
×
202
                    column = location.getColumnNumber();
×
203
                }
204
                throw originalXPathException("Could not transform input using: " + options.xsltSource._1 + ", at line: " + line + ", column: " + column + ". Error: ", e, ErrorCodes.FOXT0003);
×
205
            }
206

207
        } else {
208
            throw new XPathException(fnTransform, ErrorCodes.FOXT0001, "xslt-version: " + options.xsltVersion + " is not supported.");
1✔
209
        }
210
    }
211

212

213
    private XsltExecutable compileExecutable(final Options options) throws XPathException {
214
        final XsltCompiler xsltCompiler = context.getBroker().getBrokerPool().getSaxonProcessor().newXsltCompiler();
1✔
215
        final SingleRequestErrorListener errorListener = new SingleRequestErrorListener(Transform.ERROR_LISTENER);
1✔
216
        xsltCompiler.setErrorListener(errorListener);
1✔
217

218
        for (final Map.Entry<net.sf.saxon.s9api.QName, XdmValue> entry : options.staticParams.entrySet()) {
1✔
219
            xsltCompiler.setParameter(entry.getKey(), entry.getValue());
1✔
220
        }
221

222
        for (final IEntry<AtomicValue, Sequence> entry : options.stylesheetParams) {
1✔
223
            final QName qKey = ((QNameValue) entry.key()).getQName();
1✔
224
            final XdmValue value = toSaxon.of(entry.value());
1✔
225
            xsltCompiler.setParameter(new net.sf.saxon.s9api.QName(qKey.getPrefix(), qKey.getLocalPart()), value);
1✔
226
        }
227

228
        xsltCompiler.setURIResolver(new URIResolution.CompileTimeURIResolver(context, fnTransform) {
1✔
229
            @Override  public Source resolve(final String href, final String base) throws TransformerException {
230
                // Correct error from URI resolution when there is no base
231
                try {
232
                    final URI hrefURI = URI.create(href);
1✔
233
                    if (options.resolvedStylesheetBaseURI.isEmpty() && !hrefURI.isAbsolute() && isNullOrEmpty(base)) {
1!
234
                        final XPathException resolutionException = new XPathException(fnTransform,
1✔
235
                            ErrorCodes.XTSE0165,
1✔
236
                            "transform using a relative href, \n" +
1✔
237
                                "using option stylesheet-text, but without stylesheet-base-uri");
238
                        throw new TransformerException(resolutionException);
1✔
239
                    }
240
                } catch (final IllegalArgumentException e) {
×
241
                    throw new TransformerException(e);
×
242
                }
243
                // Checked the special error case, defer to eXist resolution
244
                return super.resolve(href, base);
×
245
            }
246
        });
247

248
        try {
249
            options.resolvedStylesheetBaseURI.ifPresent(anyURIValue -> options.xsltSource._2.setSystemId(anyURIValue.getStringValue()));
1✔
250
            return xsltCompiler.compile(options.xsltSource._2); //TODO(AR) need to implement support for xslt-packages
1✔
251
        } catch (final SaxonApiException e) {
1✔
252
            final Optional<Exception> compilerException = errorListener.getWorst().map(e1 -> e1);
1✔
253
            throw originalXPathException("Could not compile stylesheet: ", compilerException.orElse(e), ErrorCodes.FOXT0003);
1✔
254
        }
255
    }
256

257
    /**
258
     * Search for an XPathException in the cause chain, and return it "directly"
259
     * Either an eXist XPathException, which is immediate
260
     * Or a Saxon XPathException, when we convert it to something similar in eXist.
261
     *
262
     * @param e the top of the exception stack
263
     * @param defaultErrorCode use this code and its description to fill in blanks in what we finally throw
264
     * @return XPathException the eventual eXist exception which the caller is expected to throw
265
     */
266
    private XPathException originalXPathException(final String prefix, @Nonnull final Throwable e, final ErrorCodes.ErrorCode defaultErrorCode) {
267
        Throwable cause = e;
1✔
268
        while (cause != null) {
1✔
269
            if (cause instanceof XPathException) {
1✔
270
                return new XPathException(fnTransform, ((XPathException) cause).getErrorCode(), prefix + cause.getMessage());
1✔
271
            }
272
            cause = cause.getCause();
1✔
273
        }
274

275
        cause = e;
1✔
276
        while (cause != null) {
1!
277
            if (cause instanceof final net.sf.saxon.trans.XPathException xPathException) {
1!
278
                final StructuredQName from = xPathException.getErrorCodeQName();
1✔
279
                if (from != null) {
1!
280
                    final QName errorCodeQName = new QName(from.getLocalPart(), from.getURI(), from.getPrefix());
1✔
281
                    final ErrorCodes.ErrorCode errorCode = new ErrorCodes.ErrorCode(errorCodeQName, cause.getMessage());
1✔
282
                    return new XPathException(fnTransform, errorCode, prefix + cause.getMessage());
1✔
283
                } else {
284
                    return new XPathException(fnTransform, defaultErrorCode, prefix + cause.getMessage());
×
285
                }
286
            }
287
            cause = cause.getCause();
×
288
        }
289

290
        return new XPathException(fnTransform, defaultErrorCode, prefix + e.getMessage());
×
291
    }
292

293
    /**
294
     * Hash on the options used to create a compiled executable
295
     * Hash should match only when the executable can be re-used.
296
     *
297
     * @param options options to read
298
     * @return a string, the hash we want
299
     */
300
    private String executableHash(final Options options) {
301

302
        final String uniquifier;
303
        if (options.resolvedStylesheetBaseURI.isPresent() || options.sourceTextChecksum.isPresent()) {
1✔
304
            uniquifier = "";
1✔
305
        } else {
1✔
306
            uniquifier = LocalDateTime.now().toString();
1✔
307
        }
308
        final String paramHash = Tuple(
1✔
309
                options.stylesheetParams,
1✔
310
                options.staticParams).toString();
1✔
311

312
        final String locationHash = Tuple(
1✔
313
                options.resolvedStylesheetBaseURI.map(AnyURIValue::getStringValue).orElse(""),
1✔
314
                options.sourceTextChecksum.orElse(0L),
1✔
315
                uniquifier,
1✔
316
                options.stylesheetNodeDocumentPath,
1✔
317
                options.stylesheetNodeDocumentPath).toString();
1✔
318

319
        return Tuple(locationHash, paramHash).toString();
1✔
320
    }
321

322
    private class TemplateInvocation {
1✔
323

324
        final Options options;
325
        Optional<Source> sourceNode;
326
        final Delivery delivery;
327
        final Destination destination;
328
        final Xslt30Transformer xslt30Transformer;
329
        final Map<URI, Delivery> resultDocuments;
330

331
        TemplateInvocation(final Options options, final Optional<Source> sourceNode, final Delivery delivery, final Xslt30Transformer xslt30Transformer, final Map<URI, Delivery> resultDocuments) {
1✔
332
            this.options = options;
1✔
333
            this.sourceNode = sourceNode;
1✔
334
            this.delivery = delivery;
1✔
335
            this.destination = delivery.createDestination(xslt30Transformer, false);
1✔
336
            this.xslt30Transformer = xslt30Transformer;
1✔
337
            this.resultDocuments = resultDocuments;
1✔
338
        }
1✔
339

340
        private MapType invokeCallFunction() throws XPathException, SaxonApiException {
341
            assert options.initialFunction.isPresent();
1!
342
            final net.sf.saxon.s9api.QName qName = Convert.ToSaxon.of(options.initialFunction.get().getQName());
1✔
343
            final XdmValue[] functionParams;
344
            if (options.functionParams.isPresent()) {
1!
345
                functionParams = toSaxon.of(options.functionParams.get());
×
346
            } else {
×
347
                throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Error - transform using XSLT 3.0 option initial-function, but the corresponding option function-params was not supplied.");
1✔
348
            }
349

350
            xslt30Transformer.callFunction(qName, functionParams, destination);
×
351
            return makeResultMap(options, delivery, resultDocuments);
×
352
        }
353

354
        private MapType invokeCallTemplate() throws XPathException, SaxonApiException {
355
            assert options.initialTemplate.isPresent();
1!
356
            if (options.initialMode.isPresent()) {
1✔
357
                throw new XPathException(fnTransform, ErrorCodes.FOXT0002,
1✔
358
                        Options.INITIAL_MODE.name + " supplied indicating apply-templates invocation, " +
1✔
359
                                "AND " + Options.INITIAL_TEMPLATE.name + " supplied indicating call-template invocation.");
1✔
360
            }
361

362
            // Convert using our own {@link Convert} class
363
            // The saxDestination conversion loses type information in some cases
364
            // e.g. fn-transform-63 from XQTS has a <xsl:template name='main' as='xs:integer'>
365
            // which alongside "delivery-format":"raw" fails to deliver an int
366

367
            final QName qName = options.initialTemplate.get().getQName();
1✔
368
            xslt30Transformer.callTemplate(Convert.ToSaxon.of(qName), destination);
1✔
369
            return makeResultMap(options, delivery, resultDocuments);
1✔
370
        }
371

372
        private MapType invokeApplyTemplates() throws XPathException, SaxonApiException {
373
            if (options.initialMatchSelection.isPresent()) {
1✔
374
                final Sequence initialMatchSelection = options.initialMatchSelection.get();
1✔
375
                final Item item = initialMatchSelection.itemAt(0);
1✔
376
                if (item instanceof Document) {
1!
377
                    final Source sourceIMS = new DOMSource((Document)item, context.getBaseURI().getStringValue());
×
378
                    xslt30Transformer.applyTemplates(sourceIMS, destination);
×
379
                } else {
×
380
                    final XdmValue selection = toSaxon.of(initialMatchSelection);
1✔
381
                    xslt30Transformer.applyTemplates(selection, destination);
1✔
382
                }
383
            } else if (sourceNode.isPresent()) {
1!
384
                xslt30Transformer.applyTemplates(sourceNode.get(), destination);
1✔
385
            } else {
1✔
386
                throw new XPathException(fnTransform,
×
387
                        ErrorCodes.FOXT0002,
×
388
                        "One of " + Options.SOURCE_NODE.name + " or " +
×
389
                                Options.INITIAL_MATCH_SELECTION.name + " or " +
×
390
                                Options.INITIAL_TEMPLATE.name + " or " +
×
391
                                Options.INITIAL_FUNCTION.name + " is required.");
×
392
            }
393
            return makeResultMap(options, delivery, resultDocuments);
1✔
394
        }
395

396
        private MapType invoke() throws XPathException, SaxonApiException {
397
            if (options.initialFunction.isPresent()) {
1✔
398
                return invokeCallFunction();
×
399
            } else if (options.initialTemplate.isPresent()) {
1✔
400
                return invokeCallTemplate();
1✔
401
            } else {
402
                return invokeApplyTemplates();
1✔
403
            }
404
        }
405

406
        private MapType makeResultMap(final Options options, final Delivery primaryDelivery, final Map<URI, Delivery> resultDocuments) throws XPathException {
407

408
            try (final MapType outputMap = new MapType(context)) {
1✔
409
                final AtomicValue outputKey;
410
                outputKey = options.baseOutputURI.orElseGet(() -> new StringValue("output"));
1✔
411

412
                final Sequence primaryValue = postProcess(outputKey, primaryDelivery.convert(), options.postProcess);
1✔
413
                outputMap.add(outputKey, primaryValue);
1✔
414

415
                for (final Map.Entry<URI, Delivery> resultDocument : resultDocuments.entrySet()) {
1✔
416
                    final AnyURIValue key = new AnyURIValue(resultDocument.getKey());
1✔
417
                    final Delivery secondaryDelivery = resultDocument.getValue();
1✔
418
                    final Sequence value = postProcess(key, secondaryDelivery.convert(), options.postProcess);
1✔
419
                    outputMap.add(key, value);
1✔
420
                }
421

422
                return outputMap;
1✔
423
            }
424
        }
425
    }
426

427
     private Sequence postProcess(final AtomicValue key, final Sequence before, final Optional<FunctionReference> postProcessingFunction) throws XPathException {
428
        if (postProcessingFunction.isPresent()) {
1✔
429
            FunctionReference functionReference = postProcessingFunction.get();
1✔
430
            return functionReference.evalFunction(null, null, new Sequence[]{key, before});
1✔
431
        } else {
432
            return before;
1✔
433
        }
434
    }
435

436
    private static Optional<Source> getSourceNode(final Optional<NodeValue> sourceNode, final AnyURIValue baseURI) {
437
        return sourceNode.map(NodeValue::getNode).map(node -> new DOMSource(node, baseURI.getStringValue()));
1✔
438
    }
439

440
    private static class ErrorListenerLog4jAdapter implements ErrorListener {
441
        private final Logger logger;
442

443
        public ErrorListenerLog4jAdapter(final Logger logger) {
1✔
444
            this.logger = logger;
1✔
445
        }
1✔
446

447
        @Override
448
        public void warning(final TransformerException e) {
449
            logger.warn(e.getMessage(), e);
×
450
        }
×
451

452
        @Override
453
        public void error(final TransformerException e) {
454
            logger.error(e.getMessage(), e);
×
455
        }
×
456

457
        @Override
458
        public void fatalError(final TransformerException e) {
459
            logger.fatal(e.getMessage(), e);
1✔
460
        }
1✔
461
    }
462

463
    private static class SingleRequestErrorListener implements ErrorListener {
464

465
        private Optional<TransformerException> lastError;
466
        private Optional<TransformerException> lastFatal;
467

468
        public Optional<TransformerException> getWorst() {
469
            if (lastFatal.isPresent()) return lastFatal;
1!
470
            return lastError;
×
471
        }
472

473
        private final ErrorListener global;
474
        SingleRequestErrorListener(ErrorListener global) {
1✔
475
            this.global = global;
1✔
476
        }
1✔
477

478
        @Override
479
        public void warning(TransformerException exception) throws TransformerException {
480
            global.warning(exception);
×
481
        }
×
482

483
        @Override
484
        public void error(TransformerException exception) throws TransformerException {
485
            lastError = Optional.of(exception);
×
486
            global.error(exception);
×
487
        }
×
488

489
        @Override
490
        public void fatalError(TransformerException exception) throws TransformerException {
491
            lastFatal = Optional.of(exception);
1✔
492
            global.fatalError(exception);
1✔
493
        }
1✔
494
    }
495

496
    /**
497
     * A convenience for throwing a checked exception within fn:transform support code,
498
     * without the {@link XQueryContext} necessary for an immediate XPathException.
499
     *
500
     * Useful in a static helper class, for instance.
501
     */
502
    static class PendingException extends Exception {
503

504
        public PendingException(String message, Throwable cause) {
505
            super(message, cause);
1✔
506
        }
1✔
507
    }
508
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc