• 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

86.38
/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/Options.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.evolvedbinary.j8fu.tuple.Tuple2;
27
import io.lacuna.bifurcan.IEntry;
28
import net.jpountz.xxhash.XXHash64;
29
import net.jpountz.xxhash.XXHashFactory;
30
import net.sf.saxon.expr.parser.RetainedStaticContext;
31
import net.sf.saxon.functions.SystemProperty;
32
import net.sf.saxon.s9api.QName;
33
import net.sf.saxon.s9api.XdmValue;
34
import org.exist.dom.memtree.NamespaceNode;
35
import org.exist.xquery.ErrorCodes;
36
import org.exist.xquery.XPathException;
37
import org.exist.xquery.XQueryContext;
38
import org.exist.xquery.functions.array.ArrayType;
39
import org.exist.xquery.functions.fn.FnTransform;
40
import org.exist.xquery.functions.map.MapType;
41
import org.exist.xquery.value.*;
42
import org.w3c.dom.Document;
43
import org.w3c.dom.Element;
44
import org.w3c.dom.NamedNodeMap;
45
import org.w3c.dom.Node;
46

47
import javax.annotation.Nullable;
48
import javax.xml.stream.XMLEventReader;
49
import javax.xml.stream.XMLInputFactory;
50
import javax.xml.stream.XMLStreamConstants;
51
import javax.xml.stream.XMLStreamException;
52
import javax.xml.stream.events.Attribute;
53
import javax.xml.stream.events.StartElement;
54
import javax.xml.stream.events.XMLEvent;
55
import javax.xml.transform.Source;
56
import javax.xml.transform.dom.DOMSource;
57
import javax.xml.transform.stream.StreamSource;
58
import java.io.Reader;
59
import java.io.StringReader;
60
import java.math.BigDecimal;
61
import java.math.RoundingMode;
62
import java.net.URI;
63
import java.net.URISyntaxException;
64
import java.util.*;
65

66
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
67
import static java.nio.charset.StandardCharsets.UTF_8;
68
import static org.exist.Namespaces.XSL_NS;
69
import static org.exist.util.StringUtil.notNullOrEmpty;
70
import static org.exist.xquery.functions.fn.transform.Options.Option.*;
71

72
/**
73
 * Read options into class values in a single place.
74
 * <p></p>
75
 * This is a bit clearer where we need an option several times,
76
 * we know we have read it up front.
77
 */
78
class Options {
79

80
    static final javax.xml.namespace.QName QN_XSL_STYLESHEET = new javax.xml.namespace.QName(XSL_NS, "stylesheet");
1✔
81
    static final javax.xml.namespace.QName QN_VERSION = new javax.xml.namespace.QName("version");
1✔
82

83
    private static final long XXHASH64_SEED = 0x2245a28e;
84
    private static final XXHash64 XX_HASH_64 = XXHashFactory.fastestInstance().hash64();
1✔
85

86

87
    final Tuple2<String, Source> xsltSource;
88
    final MapType stylesheetParams;
89
    final Map<net.sf.saxon.s9api.QName, XdmValue> staticParams;
90
    final XSLTVersion xsltVersion;
91
    final Optional<AnyURIValue> resolvedStylesheetBaseURI;
92
    final Optional<QNameValue> initialFunction;
93
    final Optional<ArrayType> functionParams;
94
    final Map<net.sf.saxon.s9api.QName, XdmValue> templateParams;
95
    final Map<net.sf.saxon.s9api.QName, XdmValue> tunnelParams;
96
    final Optional<QNameValue> initialTemplate;
97
    final Optional<QNameValue> initialMode;
98
    final Optional<NodeValue> sourceNode;
99
    final Optional<Item> globalContextItem;
100
    final Optional<Sequence> initialMatchSelection;
101
    final Optional<BooleanValue> shouldCache;
102
    final Delivery.Format deliveryFormat;
103
    final Optional<StringValue> baseOutputURI;
104
    final Optional<MapType> serializationParams;
105

106
    final Optional<BooleanValue> enableAssertions;
107
    final Optional<BooleanValue> enableMessages;
108
    final Optional<BooleanValue> enableTrace;
109

110
    final Optional<StringValue> packageName;
111
    final Optional<StringValue> packageVersion;
112
    final Optional<StringValue> packageText;
113
    final Optional<NodeValue> packageNode;
114
    final Optional<StringValue> packageLocation;
115

116
    final Optional<MapType> vendorOptions;
117

118

119
    final Optional<Long> sourceTextChecksum;
120
    final String stylesheetNodeDocumentPath;
121

122
    final Optional<FunctionReference> postProcess;
123

124
    private final XQueryContext context;
125
    private final FnTransform fnTransform;
126
    private final Convert.ToSaxon toSaxon;
127

128
    private final SystemProperties systemProperties;
129

130
    Options(final XQueryContext context, final FnTransform fnTransform, final Convert.ToSaxon toSaxon, final MapType options) throws XPathException {
1✔
131
        this.context = context;
1✔
132
        this.fnTransform = fnTransform;
1✔
133
        this.toSaxon = toSaxon;
1✔
134
        this.systemProperties = new SystemProperties(context);
1✔
135

136
        xsltSource = getStylesheet(options);
1✔
137

138
        stylesheetParams = Options.STYLESHEET_PARAMS.get(options).orElse(new MapType(context));
1✔
139
        for (final IEntry<AtomicValue, Sequence> entry : stylesheetParams) {
1✔
140
            if (!(entry.key() instanceof QNameValue)) {
1✔
141
                throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied stylesheet-param is not a valid xs:qname: " + entry);
1✔
142
            }
143
            if (entry.value() == null) {
1!
144
                throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied stylesheet-param is not a valid xs:sequence: " + entry);
×
145
            }
146
        }
147

148
        final Optional<DecimalValue> explicitXsltVersion = Options.XSLT_VERSION.get(options);
1✔
149
        if (explicitXsltVersion.isPresent()) {
1✔
150
            try {
151
                xsltVersion = XSLTVersion.fromDecimal(explicitXsltVersion.get().getValue());
1✔
152
                if (xsltVersion.equals(V1_0) && xsltVersion.equals(V2_0) && xsltVersion.equals(V3_0)) {
1!
153
                    throw new XPathException(fnTransform, ErrorCodes.FOXT0001, "Supplied xslt-version is an unknown XSLT version: " + explicitXsltVersion.get());
×
154
                }
155
            } catch (final Transform.PendingException pe) {
×
156
                throw new XPathException(fnTransform, ErrorCodes.FOXT0001, "Supplied xslt-version is an unknown XSLT version: " + explicitXsltVersion.get());
×
157
            }
158
        } else {
159
            xsltVersion = getXsltVersion(xsltSource._2);
1✔
160
        }
161

162
        final String stylesheetBaseUri;
163
        final Optional<StringValue> explicitStylesheetBaseUri = Options.STYLESHEET_BASE_URI.get(xsltVersion, options);
1✔
164
        if (explicitStylesheetBaseUri.isPresent()) {
1!
165
            stylesheetBaseUri = explicitStylesheetBaseUri.get().getStringValue();
×
166
        } else {
×
167
            stylesheetBaseUri = xsltSource._1;
1✔
168
        }
169
        if (notNullOrEmpty(stylesheetBaseUri)) {
1✔
170
            resolvedStylesheetBaseURI = Optional.of(resolveURI(new AnyURIValue(stylesheetBaseUri), context.getBaseURI()));
1✔
171
        } else {
1✔
172
            resolvedStylesheetBaseURI = Optional.empty();
1✔
173
        }
174
        initialFunction = Options.INITIAL_FUNCTION.get(options);
1✔
175
        functionParams = Options.FUNCTION_PARAMS.get(options);
1✔
176

177
        initialTemplate = Options.INITIAL_TEMPLATE.get(options);
1✔
178
        initialMode = Options.INITIAL_MODE.get(options);
1✔
179

180
        templateParams = readParamsMap(Options.TEMPLATE_PARAMS.get(options), Options.TEMPLATE_PARAMS.name.getStringValue());
1✔
181
        tunnelParams = readParamsMap(Options.TUNNEL_PARAMS.get(options), Options.TUNNEL_PARAMS.name.getStringValue());
1✔
182
        staticParams = readParamsMap(Options.STATIC_PARAMS.get(options), Options.STATIC_PARAMS.name.getStringValue());
1✔
183

184
        sourceNode = Options.SOURCE_NODE.get(options);
1✔
185
        globalContextItem = Options.GLOBAL_CONTEXT_ITEM.get(options);
1✔
186
        initialMatchSelection = Options.INITIAL_MATCH_SELECTION.get(options);
1✔
187
        if (sourceNode.isPresent() && initialMatchSelection.isPresent()) {
1✔
188
            throw new XPathException(ErrorCodes.FOXT0002,
1✔
189
                    "Both " + SOURCE_NODE.name + " and " + INITIAL_MATCH_SELECTION.name + " were supplied. " +
1✔
190
                            "These options cannot both be supplied.");
191
        }
192
        sourceTextChecksum = getSourceTextChecksum(options);
1✔
193
        stylesheetNodeDocumentPath = getStylesheetNodeDocumentPath(options);
1✔
194

195
        shouldCache = Options.CACHE.get(xsltVersion, options);
1✔
196

197
        deliveryFormat = getDeliveryFormat(xsltVersion, options);
1✔
198

199
        baseOutputURI = Options.BASE_OUTPUT_URI.get(xsltVersion, options);
1✔
200

201
        serializationParams = Options.SERIALIZATION_PARAMS.get(xsltVersion, options);
1✔
202

203
        validateRequestedProperties(Options.REQUESTED_PROPERTIES.get(xsltVersion, options).orElse(new MapType(context)));
1✔
204

205
        postProcess = Options.POST_PROCESS.get(xsltVersion, options);
1✔
206

207
        enableAssertions = Options.ENABLE_ASSERTIONS.get(xsltVersion, options);
1✔
208
        enableMessages = Options.ENABLE_MESSAGES.get(xsltVersion, options);
1✔
209
        enableTrace = Options.ENABLE_TRACE.get(xsltVersion, options);
1✔
210

211
        packageName = Options.PACKAGE_NAME.get(xsltVersion, options);
1✔
212
        packageVersion = Options.PACKAGE_VERSION.get(xsltVersion, options);
1✔
213
        packageText = Options.PACKAGE_TEXT.get(xsltVersion, options);
1✔
214
        packageNode = Options.PACKAGE_NODE.get(xsltVersion, options);
1✔
215
        packageLocation = Options.PACKAGE_LOCATION.get(xsltVersion, options);
1✔
216

217
        vendorOptions = Options.VENDOR_OPTIONS.get(xsltVersion, options);
1✔
218
    }
1✔
219

220
    private Map<QName, XdmValue> readParamsMap(final Optional<MapType> option, final String name) throws XPathException {
221

222
        final Map<net.sf.saxon.s9api.QName, XdmValue> result = new HashMap<>();
1✔
223

224
        final MapType paramsMap = option.orElse(new MapType(context));
1✔
225
        for (final IEntry<AtomicValue, Sequence> entry : paramsMap) {
1✔
226
            final AtomicValue key = entry.key();
1✔
227
            if (!(key instanceof QNameValue)) {
1!
228
                throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied " + name + " is not a valid xs:qname: " + entry);
×
229
            }
230
            if (entry.value() == null) {
1!
231
                throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Supplied " + name + " is not a valid xs:sequence: " + entry);
×
232
            }
233
            result.put(Convert.ToSaxon.of((QNameValue) key), toSaxon.of(entry.value()));
1✔
234
        }
235
        return result;
1✔
236
    }
237

238
    private Delivery.Format getDeliveryFormat(final XSLTVersion xsltVersion, final MapType options) throws XPathException {
239
        final String deliveryFormatString = Options.DELIVERY_FORMAT.get(xsltVersion, options).orElse(new StringValue(Delivery.Format.DOCUMENT.name())).getStringValue().toUpperCase();
1✔
240
        final Delivery.Format format;
241
        try {
242
            format = Delivery.Format.valueOf(deliveryFormatString);
1✔
243
        } catch (final IllegalArgumentException ie) {
1✔
244
            throw new XPathException(fnTransform, ErrorCodes.FOXT0002,
×
245
                    ": \"" + deliveryFormatString + "\" is not a valid " + Options.DELIVERY_FORMAT.name);
×
246
        }
247
        return format;
1✔
248
    }
249

250
    private Optional<Long> getSourceTextChecksum(final MapType options) throws XPathException {
251
        final Optional<String> stylesheetText = Options.STYLESHEET_TEXT.get(options).map(StringValue::getStringValue);
1✔
252
        if (stylesheetText.isPresent()) {
1✔
253
            final String text = stylesheetText.get();
1✔
254
            final byte[] data = text.getBytes(UTF_8);
1✔
255
            return Optional.of(Options.XX_HASH_64.hash(data, 0, data.length, Options.XXHASH64_SEED));
1✔
256
        }
257
        return Optional.empty();
1✔
258
    }
259

260
    private String getStylesheetNodeDocumentPath(final MapType options) throws XPathException {
261
        final Optional<Node> stylesheetNode = Options.STYLESHEET_NODE.get(options).map(NodeValue::getNode);
1✔
262
        return stylesheetNode.map(node -> TreeUtils.pathTo(node).toString()).orElse("");
1✔
263
    }
264

265
    private void validateRequestedProperties(final MapType requestedProperties) throws XPathException {
266
        for (final IEntry<AtomicValue, Sequence> entry : requestedProperties) {
1✔
267
            final AtomicValue key = entry.key();
1✔
268
            if (!Type.subTypeOf(key.getType(), Type.QNAME)) {
1✔
269
                throw new XPathException(ErrorCodes.XPTY0004, "Type error: requested-properties key: " + key + " is not a QName");
1✔
270
            }
271
            final Sequence value = entry.value();
1✔
272
            if (!value.hasOne()) {
1✔
273
                throw new XPathException(ErrorCodes.XPTY0004, "Type error: requested-properties " + key + " does not have a single item value.");
1✔
274
            }
275
            final Item item = value.itemAt(0);
1✔
276
            final String requiredPropertyValue;
277
            if (Type.subTypeOf(item.getType(), Type.STRING)) {
1✔
278
                requiredPropertyValue = item.getStringValue();
1✔
279
            } else if (Type.subTypeOf(item.getType(), Type.BOOLEAN)) {
1✔
280
                requiredPropertyValue = ((BooleanValue) item).getValue() ? "yes" : "no";
1✔
281
            } else {
1✔
282
                throw new XPathException(ErrorCodes.XPTY0004,
1✔
283
                        "Type error: requested-properties " + key +
1✔
284
                                " is not a " + Type.getTypeName(Type.STRING) +
1✔
285
                                " or a " + Type.getTypeName(Type.BOOLEAN));
1✔
286
            }
287
            final String actualPropertyValue = systemProperties.get(((QNameValue) key).getQName());
1✔
288
            if (!actualPropertyValue.equalsIgnoreCase(requiredPropertyValue)) {
1✔
289
                throw new XPathException(ErrorCodes.FOXT0001,
1✔
290
                        "The XSLT processor cannot provide the requested-property: " + key +
1✔
291
                                " requested: " + requiredPropertyValue +
1✔
292
                                ", actual: " + actualPropertyValue);
1✔
293
            }
294
        }
295
    }
1✔
296
    private static final Option<StringValue> BASE_OUTPUT_URI = new ItemOption<>(
1✔
297
            Type.STRING, "base-output-uri", V1_0, V2_0, V3_0);
1✔
298
    private static final Option<BooleanValue> CACHE = new ItemOption<>(
1✔
299
            Type.BOOLEAN, "cache", BooleanValue.TRUE, V1_0, V2_0, V3_0);
1✔
300
    private static final Option<StringValue> DELIVERY_FORMAT = new ItemOption<>(
1✔
301
            Type.STRING, "delivery-format", new StringValue("document"), V1_0, V2_0, V3_0);
1✔
302
    private static final Option<BooleanValue> ENABLE_ASSERTIONS = new ItemOption<>(
1✔
303
            Type.BOOLEAN, "enable-assertions", BooleanValue.FALSE, V3_0);
1✔
304
    private static final Option<BooleanValue> ENABLE_MESSAGES = new ItemOption<>(
1✔
305
            Type.BOOLEAN, "enable-messages", BooleanValue.TRUE, V1_0, V2_0, V3_0);
1✔
306
    private static final Option<BooleanValue> ENABLE_TRACE = new ItemOption<>(
1✔
307
            Type.BOOLEAN, "enable-trace", BooleanValue.TRUE, V2_0, V3_0);
1✔
308
    private static final Option<ArrayType> FUNCTION_PARAMS = new ItemOption<>(
1✔
309
            Type.ARRAY_ITEM,"function-params", V3_0);
1✔
310
    private static final Option<Item> GLOBAL_CONTEXT_ITEM = new ItemOption<>(
1✔
311
            Type.ITEM, "global-context-item", V3_0);
1✔
312
    static final Option<QNameValue> INITIAL_FUNCTION = new ItemOption<>(
1✔
313
            Type.QNAME,"initial-function", V3_0);
1✔
314
    static final Option<Sequence> INITIAL_MATCH_SELECTION = new SequenceOption<>(
1✔
315
            Type.ANY_ATOMIC_TYPE,Type.NODE, "initial-match-selection", V3_0);
1✔
316
    static final Option<QNameValue> INITIAL_MODE = new ItemOption<>(
1✔
317
            Type.QNAME,"initial-mode", V1_0, V2_0, V3_0);
1✔
318
    static final Option<QNameValue> INITIAL_TEMPLATE = new ItemOption<>(
1✔
319
            Type.QNAME,"initial-template", V2_0, V3_0);
1✔
320
    private static final Option<StringValue> PACKAGE_NAME = new ItemOption<>(
1✔
321
            Type.STRING,"package-name", V3_0);
1✔
322
    private static final Option<StringValue> PACKAGE_LOCATION = new ItemOption<>(
1✔
323
            Type.STRING,"package-location", V3_0);
1✔
324
    private static final Option<NodeValue> PACKAGE_NODE = new ItemOption<>(
1✔
325
            Type.NODE,"package-node", V3_0);
1✔
326
    private static final Option<StringValue> PACKAGE_TEXT = new ItemOption<>(
1✔
327
            Type.STRING,"package-text", V3_0);
1✔
328
    private static final Option<StringValue> PACKAGE_VERSION = new ItemOption<>(
1✔
329
            Type.STRING,"package-version", new StringValue("*"), V3_0);
1✔
330
    private static final Option<FunctionReference> POST_PROCESS = new ItemOption<>(
1✔
331
            Type.FUNCTION,"post-process", V1_0, V2_0, V3_0);
1✔
332
    private static final Option<MapType> REQUESTED_PROPERTIES = new ItemOption<>(
1✔
333
            Type.MAP_ITEM,"requested-properties", V1_0, V2_0, V3_0);
1✔
334
    private static final Option<MapType> SERIALIZATION_PARAMS = new ItemOption<>(
1✔
335
            Type.MAP_ITEM,"serialization-params", V1_0, V2_0, V3_0);
1✔
336
    static final Option<NodeValue> SOURCE_NODE = new ItemOption<>(
1✔
337
            Type.NODE,"source-node", V1_0, V2_0, V3_0);
1✔
338
    private static final Option<MapType> STATIC_PARAMS = new ItemOption<>(
1✔
339
            Type.MAP_ITEM,"static-params", V3_0);
1✔
340
    private static final Option<StringValue> STYLESHEET_BASE_URI = new ItemOption<>(
1✔
341
            Type.STRING, "stylesheet-base-uri", V1_0, V2_0, V3_0);
1✔
342
    static final Option<StringValue> STYLESHEET_LOCATION = new ItemOption<>(
1✔
343
            Type.STRING,"stylesheet-location", V1_0, V2_0, V3_0);
1✔
344
    static final Option<NodeValue> STYLESHEET_NODE = new ItemOption<>(
1✔
345
            Type.NODE,"stylesheet-node", V1_0, V2_0, V3_0);
1✔
346
    private static final Option<MapType> STYLESHEET_PARAMS = new ItemOption<>(
1✔
347
            Type.MAP_ITEM,"stylesheet-params", V1_0, V2_0, V3_0);
1✔
348
    static final Option<StringValue> STYLESHEET_TEXT = new ItemOption<>(
1✔
349
            Type.STRING,"stylesheet-text", V1_0, V2_0, V3_0);
1✔
350
    private static final Option<MapType> TEMPLATE_PARAMS = new ItemOption<>(
1✔
351
            Type.MAP_ITEM,"template-params", V3_0);
1✔
352
    private static final Option<MapType> TUNNEL_PARAMS = new ItemOption<>(
1✔
353
            Type.MAP_ITEM,"tunnel-params", V3_0);
1✔
354
    private static final Option<MapType> VENDOR_OPTIONS = new ItemOption<>(
1✔
355
            Type.MAP_ITEM,"vendor-options", V1_0, V2_0, V3_0);
1✔
356
    private static final Option<DecimalValue> XSLT_VERSION = new ItemOption<>(
1✔
357
            Type.DECIMAL,"xslt-version", V1_0, V2_0, V3_0);
1✔
358

359
    abstract static class Option<T> {
360
        public static final XSLTVersion V1_0 = new XSLTVersion(1,0);
1✔
361
        public static final XSLTVersion V2_0 = new XSLTVersion(2,0);
1✔
362
        public static final XSLTVersion V3_0 = new XSLTVersion(3,0);
1✔
363

364
        protected final StringValue name;
365
        protected final Optional<T> defaultValue;
366
        protected final XSLTVersion[] appliesToVersions;
367
        protected final int itemSubtype;
368

369
        private Option(final int itemSubtype, final String name, final Optional<T> defaultValue, final XSLTVersion... appliesToVersions) {
1✔
370
            this.name = new StringValue(name);
1✔
371
            this.defaultValue = defaultValue;
1✔
372
            this.appliesToVersions = appliesToVersions;
1✔
373
            this.itemSubtype = itemSubtype;
1✔
374
        }
1✔
375

376
        public abstract Optional<T> get(final MapType options) throws XPathException;
377

378
        private boolean notApplicableToVersion(final XSLTVersion xsltVersion) {
379
            for (final XSLTVersion appliesToVersion : appliesToVersions) {
1✔
380
                if (xsltVersion.equals(appliesToVersion)) {
1✔
381
                    return false;
1✔
382
                }
383
            }
384
            return true;
1✔
385
        }
386

387
        public Optional<T> get(final XSLTVersion xsltVersion, final MapType options) throws XPathException {
388
            if (notApplicableToVersion(xsltVersion)) {
1✔
389
                return Optional.empty();
1✔
390
            }
391

392
            return get(options);
1✔
393
        }
394
    }
395

396
    static class SequenceOption<T extends Sequence> extends Option<T> {
397

398
        private final int sequenceSubtype;
399

400
        public SequenceOption(final int sequenceSubtype, final int itemSubtype, final String name, final XSLTVersion... appliesToVersions) {
401
            super(itemSubtype, name, Optional.empty(), appliesToVersions);
1✔
402
            this.sequenceSubtype = sequenceSubtype;
1✔
403
        }
1✔
404

405
        public SequenceOption(final int sequenceSubtype, final int itemSubtype, final String name, @Nullable final T defaultValue, final XSLTVersion... appliesToVersions) {
406
            super(itemSubtype, name, Optional.ofNullable(defaultValue), appliesToVersions);
×
407
            this.sequenceSubtype = sequenceSubtype;
×
408
        }
×
409

410
        @Override
411
        public Optional<T> get(final MapType options) throws XPathException {
412
            if (options.contains(name)) {
1✔
413
                final Sequence sequence = options.get(name);
1✔
414
                if (Type.subTypeOf(sequence.getItemType(), sequenceSubtype)) {
1!
415
                    return Optional.of((T) sequence);
1✔
416
                }
417
                final Item item0 = options.get(name).itemAt(0);
×
418
                if (item0 != null) {
×
419
                    if (Type.subTypeOf(item0.getType(), itemSubtype)) {
×
420
                        return Optional.of((T) item0);
×
421
                    } else {
422
                        throw new XPathException(
×
423
                                ErrorCodes.XPTY0004, "Type error: expected " + Type.getTypeName(itemSubtype) + ", got " + Type.getTypeName(sequence.getItemType()));
×
424
                    }
425
                }
426
            }
427
            return defaultValue;
1✔
428
        }
429
    }
430

431
    static class ItemOption<T extends Item> extends Option<T> {
432

433
        public ItemOption(final int itemSubtype, final String name, final XSLTVersion... appliesToVersions) {
434
            super(itemSubtype, name, Optional.empty(), appliesToVersions);
1✔
435
        }
1✔
436

437
        public ItemOption(final int itemSubtype, final String name, @Nullable final T defaultValue, final XSLTVersion... appliesToVersions) {
438
            super(itemSubtype, name, Optional.ofNullable(defaultValue), appliesToVersions);
1✔
439
        }
1✔
440

441
        @Override
442
        public Optional<T> get(final MapType options) throws XPathException {
443
            if (options.contains(name)) {
1✔
444
                final Item item0 = options.get(name).itemAt(0);
1✔
445
                if (item0 != null) {
1✔
446
                    if (Type.subTypeOf(item0.getType(), itemSubtype)) {
1✔
447
                        return Optional.of((T) item0);
1✔
448
                    } else if (itemSubtype == Type.STRING && Type.subTypeOf(item0.getType(), Type.ANY_ATOMIC_TYPE)) {
1!
449
                        return Optional.of((T)new StringValue(item0.getStringValue()));
1✔
450
                    } else {
451
                        throw new XPathException(
1✔
452
                                ErrorCodes.XPTY0004, "Type error: expected " + Type.getTypeName(itemSubtype) + ", got " + Type.getTypeName(item0.getType()));
1✔
453
                    }
454
                }
455
            }
456
            return defaultValue;
1✔
457
        }
458
    }
459

460
    /**
461
     * Get the stylesheet.
462
     *
463
     * @return a Tuple whose first value is the stylesheet base uri, and whose second
464
     *     value is the source for accessing the stylesheet
465
     */
466
    private Tuple2<String, Source> getStylesheet(final MapType options) throws XPathException {
467

468
        final List<Tuple2<String, Source>> results = new ArrayList<>(1);
1✔
469
        final Optional<String> stylesheetLocation = Options.STYLESHEET_LOCATION.get(options).map(StringValue::getStringValue);
1✔
470
        if (stylesheetLocation.isPresent()) {
1✔
471
            results.add(Tuple(stylesheetLocation.get(), resolveStylesheetLocation(stylesheetLocation.get())));
1✔
472
        }
473

474
        final Optional<Node> stylesheetNode = Options.STYLESHEET_NODE.get(options).map(NodeValue::getNode);
1✔
475
        if (stylesheetNode.isPresent()) {
1✔
476
            final Node node = stylesheetNode.get();
1✔
477
            results.add(Tuple(node.getBaseURI(), new DOMSource(node)));
1✔
478
        }
479

480
        final Optional<String> stylesheetText = Options.STYLESHEET_TEXT.get(options).map(StringValue::getStringValue);
1✔
481
        stylesheetText.ifPresent(s -> results.add(Tuple("", new StringSource(s))));
1✔
482

483
        if (results.size() > 1) {
1✔
484
            throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "More than one of stylesheet-location, stylesheet-node, and stylesheet-text was set");
1✔
485
        }
486

487
        if (results.isEmpty()) {
1✔
488
            throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "None of stylesheet-location, stylesheet-node, or stylesheet-text was set");
1✔
489
        }
490

491
        return results.get(0);
1✔
492
    }
493

494
    /**
495
     * Resolve the stylesheet location.
496
     * <p>
497
     *     It may be a dynamically configured document.
498
     *     Or a document within the database.
499
     * </p>
500
     * @param stylesheetLocation path or URI of stylesheet
501
     * @return a source wrapping the contents of the stylesheet
502
     * @throws XPathException if there is a problem resolving the location.
503
     */
504
    private Source resolveStylesheetLocation(final String stylesheetLocation) throws XPathException {
505

506
        final URI uri = URI.create(stylesheetLocation);
1✔
507
        if (uri.isAbsolute()) {
1✔
508
            return URIResolution.resolveDocument(stylesheetLocation, context, fnTransform);
1✔
509
        } else {
510
            final AnyURIValue resolved = resolveURI(new AnyURIValue(stylesheetLocation), context.getBaseURI());
1✔
511
            return URIResolution.resolveDocument(resolved.getStringValue(), context, fnTransform);
×
512
        }
513
    }
514

515
    /**
516
     * URI resolution, the core should be the same as for fn:resolve-uri
517
     * @param relative URI to resolve
518
     * @param base to resolve against
519
     * @return resolved URI
520
     * @throws XPathException if resolution is not possible
521
     */
522
    private AnyURIValue resolveURI(final AnyURIValue relative, final AnyURIValue base) throws XPathException {
523
        try {
524
            return URIResolution.resolveURI(relative, base);
1✔
525
        } catch (final URISyntaxException e) {
×
526
            throw new XPathException(fnTransform, ErrorCodes.FORG0009, "unable to resolve a relative URI against a base URI in fn:transform(): " + e.getMessage(), null, e);
×
527
        }
528
    }
529

530
    private XSLTVersion getXsltVersion(final Source xsltStylesheet) throws XPathException {
531

532
        if (xsltStylesheet instanceof DOMSource) {
1✔
533
            return domExtractXsltVersion(xsltStylesheet);
1✔
534
        } else if (xsltStylesheet instanceof StreamSource) {
1!
535
            return staxExtractXsltVersion(xsltStylesheet);
1✔
536
        }
537

538
        throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT, unrecognised source");
×
539
    }
540

541
    private XSLTVersion domExtractXsltVersion(final Source xsltStylesheet) throws XPathException {
542

543
        Node node = ((DOMSource) xsltStylesheet).getNode();
1✔
544
        if (node instanceof Document) {
1✔
545
            node = ((Document) node).getDocumentElement();
1✔
546
        }
547

548
        String version = "";
1✔
549

550
        if (node instanceof Element) {
1!
551

552
            final Element elem = (Element) node;
1✔
553
            if (XSL_NS.equals(node.getNamespaceURI())
1✔
554
                    && "stylesheet".equals(node.getLocalName())) {
1!
555
                version = elem.getAttribute("version");
1✔
556
            }
557

558
            // No luck ? Search the attributes of a "simplified stylesheet"
559
            final NamedNodeMap attributes = elem.getAttributes();
1✔
560
            for (int i = 0; version.isEmpty() && i < attributes.getLength(); i++) {
1!
561
                if (attributes.item(i) instanceof NamespaceNode) {
1✔
562
                    final NamespaceNode nsNode = (NamespaceNode) attributes.item(i);
1✔
563
                    final String uri = nsNode.getNodeValue();
1✔
564
                    final String localName = nsNode.getLocalName(); // "xsl"
1✔
565
                    if (XSL_NS.equals(uri)) {
1!
566
                        version = elem.getAttribute(localName + ":version");
1✔
567
                    }
568
                }
569
            }
570
        }
571

572
        if (version.isEmpty()) {
1!
573
            throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via DOM");
×
574
        }
575

576
        try {
577
            return XSLTVersion.fromDecimal(new BigDecimal(version));
1✔
578
        } catch (final Transform.PendingException pe) {
×
579
            throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via DOM. Value: " + version + " : " + pe.getMessage());
×
580
        }
581
    }
582

583
    private XSLTVersion staxExtractXsltVersion(final Source xsltStylesheet) throws XPathException {
584
        try {
585
            final XMLInputFactory factory = XMLInputFactory.newInstance();
1✔
586
            // Sonartype checker needs this https://rules.sonarsource.com/java/RSPEC-2755
587
            factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
1✔
588

589
            final XMLEventReader eventReader =
1✔
590
                    factory.createXMLEventReader(xsltStylesheet);
1✔
591

592
            while (eventReader.hasNext()) {
1!
593
                final XMLEvent event = eventReader.nextEvent();
1✔
594
                if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
1✔
595
                    final StartElement startElement = event.asStartElement();
1✔
596
                    if (Options.QN_XSL_STYLESHEET.equals(startElement.getName())) {
1!
597
                        final Attribute version = startElement.getAttributeByName(Options.QN_VERSION);
1✔
598
                        return XSLTVersion.fromDecimal(new BigDecimal(version.getValue()));
1✔
599
                    }
600
                }
601
            }
602
        } catch (final XMLStreamException | Transform.PendingException e) {
×
603
            throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via STaX: " + e.getMessage(), Sequence.EMPTY_SEQUENCE, e);
×
604
        }
605

606
        throw new XPathException(fnTransform, ErrorCodes.FOXT0002, "Unable to extract version from XSLT via STaX");
×
607
    }
608

609
    private static class StringSource extends StreamSource {
610

611
        private final String string;
612

613
        public StringSource(final String string) {
1✔
614
            this.string = string;
1✔
615
        }
1✔
616

617
        @Override
618
        public Reader getReader() {
619
            return new StringReader(string);
1✔
620
        }
621
    }
622

623
    static class SystemProperties {
624

625
        private final RetainedStaticContext retainedStaticContext;
626

627
        private SystemProperties(final XQueryContext context) {
1✔
628
            final var saxonConfiguration = context.getBroker().getBrokerPool().getSaxonConfiguration();
1✔
629
            this.retainedStaticContext = new RetainedStaticContext(saxonConfiguration);
1✔
630
        }
1✔
631

632
        String get(final org.exist.dom.QName qName) {
633
            return SystemProperty.getProperty(qName.getNamespaceURI(), qName.getLocalPart(), retainedStaticContext);
1✔
634
        }
635
    }
636

637
    static class XSLTVersion {
638
        final int major;
639
        final int minor;
640

641
        XSLTVersion(final int major, final int minor) {
1✔
642
            this.major = major;
1✔
643
            this.minor = minor;
1✔
644
        }
1✔
645

646
        public static XSLTVersion fromDecimal(final BigDecimal decimal) throws Transform.PendingException {
647
            final BigDecimal major = decimal.setScale(0, RoundingMode.FLOOR);
1✔
648
            final BigDecimal minor = decimal.subtract(major).multiply(BigDecimal.TEN);
1✔
649
            try {
650
                return new XSLTVersion(major.intValueExact(), minor.intValueExact());
1✔
651
            } catch (final ArithmeticException ae) {
1✔
652
                throw new Transform.PendingException("XSLT Version is not an exact X.Y value: " + decimal, ae);
1✔
653
            }
654
        }
655

656
        @Override
657
        public boolean equals(final Object o) {
658
            if (this == o) {
1!
659
                return true;
×
660
            }
661
            if (o == null || getClass() != o.getClass()) {
1!
662
                return false;
×
663
            }
664
            final XSLTVersion version = (XSLTVersion) o;
1✔
665
            return major == version.major && minor == version.minor;
1✔
666
        }
667

668
        @Override
669
        public int hashCode() {
670
            return Objects.hash(major, minor);
×
671
        }
672

673
        @Override
674
        public String toString() {
675
            return major + "." + minor;
1✔
676
        }
677
    }
678
}
679

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