• 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

76.02
/exist-core/src/main/java/org/exist/xquery/util/SerializerUtils.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
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.xquery.util;
50

51
import io.lacuna.bifurcan.IEntry;
52
import it.unimi.dsi.fastutil.Hash;
53
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
54
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
55
import org.exist.Namespaces;
56
import org.exist.dom.QName;
57
import org.exist.numbering.NodeId;
58
import org.exist.stax.ExtendedXMLStreamReader;
59
import org.exist.storage.serializers.EXistOutputKeys;
60
import org.exist.xquery.Cardinality;
61
import org.exist.xquery.ErrorCodes;
62
import org.exist.xquery.Expression;
63
import org.exist.xquery.XPathException;
64
import org.exist.xquery.functions.fn.FnModule;
65
import org.exist.xquery.functions.map.AbstractMapType;
66
import org.exist.xquery.functions.map.MapType;
67
import org.exist.xquery.value.*;
68

69
import javax.xml.XMLConstants;
70
import javax.xml.stream.XMLStreamConstants;
71
import javax.xml.stream.XMLStreamException;
72
import javax.xml.stream.XMLStreamReader;
73
import javax.xml.transform.OutputKeys;
74
import java.io.IOException;
75
import java.util.*;
76
import java.util.function.Function;
77

78
import static java.nio.charset.StandardCharsets.UTF_8;
79
import static org.exist.util.StringUtil.isNullOrEmpty;
80

81
/**
82
 * Serializer utilities used by several XQuery functions.
83
 */
84
public class SerializerUtils {
×
85

86
    // Constant strings
87
    public static final String OUTPUT_NAMESPACE = "output";
88
    public static final String CHARACTER_MAP_ELEMENT_KEY = "character-map";
89
    public static final String CHARACTER_ATTR_KEY = "character";
90
    public static final String MAP_STRING_ATTR_KEY = "map-string";
91

92
    public static final String MSG_NON_VALUE_ATTRIBUTE = "Attribute other than value in serialization parameter";
93

94
    public static final String MSG_UNSUPPORTED_TYPE = "Unsupported type ";
95
    public static final String MSG_FOR_PARAMETER_VALUE =  " for parameter value";
96

97
    private static final Set<String> W3CParameterConventionKeys = new HashSet<>();
1✔
98
    static {
99
        for (W3CParameterConvention convention : W3CParameterConvention.values()) {
1✔
100
            W3CParameterConventionKeys.add(convention.getParameterName());
1✔
101
        }
102
    }
103

104
    private static final Map<String, ParameterConvention<String>> W3C_PARAMETER_CONVENTIONS_BY_NAME = new HashMap<>();
1✔
105
    static {
106
        for (final W3CParameterConvention w3cParameterConvention : W3CParameterConvention.values()) {
1✔
107
            W3C_PARAMETER_CONVENTIONS_BY_NAME.put(w3cParameterConvention.getParameterName(), w3cParameterConvention);
1✔
108
        }
109
    }
1✔
110

111
    public interface ParameterConvention<T> {
112

113
        /**
114
         * Get the Parameter name.
115
         *
116
         * @return the parameter name
117
         */
118
        T getParameterName();
119

120
        /**
121
         * Get the Local name (i.e. without prefix or namespace)
122
         * of the parameter.
123
         *
124
         * @return the local parameter name
125
         */
126
        String getLocalParameterName();
127

128
        /**
129
         * Get the Type of the parameter.
130
         *
131
         * @return the type
132
         */
133
        int getType();
134

135
        /**
136
         * Get the Cardinality of the parameter.
137
         *
138
         * @return the cardinality
139
         */
140
        Cardinality getCardinality();
141

142
        /**
143
         * Get the Default Value of the parameter.
144
         *
145
         * @return the default value
146
         */
147
        Sequence getDefaultValue();
148
    }
149

150
    /**
151
     * See <a href="https://www.w3.org/TR/xpath-functions-31/#func-serialize">fn:serialize</a>
152
     */
153
    public enum W3CParameterConvention implements ParameterConvention<String> {
1✔
154
        ALLOW_DUPLICATE_NAMES("allow-duplicate-names", Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.FALSE),
1✔
155
        BYTE_ORDER_MARK("byte-order-mark", Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.FALSE),
1✔
156
        CDATA_SECTION_ELEMENTS(OutputKeys.CDATA_SECTION_ELEMENTS, Type.QNAME, Cardinality.ZERO_OR_MORE, Sequence.EMPTY_SEQUENCE),
1✔
157
        DOCTYPE_PUBLIC(OutputKeys.DOCTYPE_PUBLIC, Type.STRING, Cardinality.ZERO_OR_ONE, Sequence.EMPTY_SEQUENCE),   //default: () means "absent"
1✔
158
        DOCTYPE_SYSTEM(OutputKeys.DOCTYPE_SYSTEM, Type.STRING, Cardinality.ZERO_OR_ONE, Sequence.EMPTY_SEQUENCE),   //default: () means "absent"
1✔
159
        ENCODING(OutputKeys.ENCODING, Type.STRING, Cardinality.ZERO_OR_ONE, new StringValue(UTF_8.name())),
1✔
160
        ESCAPE_URI_ATTRIBUTES("escape-uri-attributes", Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.TRUE),
1✔
161
        HTML_VERSION(EXistOutputKeys.HTML_VERSION, Type.DECIMAL, Cardinality.ZERO_OR_ONE, new DecimalValue(5)),
1✔
162
        INCLUDE_CONTENT_TYPE("include-content-type", Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.TRUE),
1✔
163
        INDENT(OutputKeys.INDENT, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.FALSE),
1✔
164
        ITEM_SEPARATOR(EXistOutputKeys.ITEM_SEPARATOR, Type.STRING, Cardinality.ZERO_OR_ONE, Sequence.EMPTY_SEQUENCE),  //default: () means "absent"
1✔
165
        JSON_NODE_OUTPUT_METHOD(EXistOutputKeys.JSON_NODE_OUTPUT_METHOD, Type.STRING, Cardinality.ZERO_OR_ONE, new StringValue("xml")),
1✔
166
        MEDIA_TYPE(OutputKeys.MEDIA_TYPE, Type.STRING, Cardinality.ZERO_OR_ONE, Sequence.EMPTY_SEQUENCE),    // default: a media type suitable for the chosen method
1✔
167
        METHOD(OutputKeys.METHOD, Type.STRING, Cardinality.ZERO_OR_ONE, new StringValue("xml")),
1✔
168
        NORMALIZATION_FORM("normalization-form", Type.STRING, Cardinality.ZERO_OR_ONE, new StringValue("none")),
1✔
169
        OMIT_XML_DECLARATION(OutputKeys.OMIT_XML_DECLARATION, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.TRUE),
1✔
170
        STANDALONE(OutputKeys.STANDALONE, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, Sequence.EMPTY_SEQUENCE),   //default: () means "omit"
1✔
171
        SUPPRESS_INDENTATION("suppress-indentation", Type.QNAME, Cardinality.ZERO_OR_MORE, Sequence.EMPTY_SEQUENCE),
1✔
172
        UNDECLARE_PREFIXES("undeclare-prefixes", Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.FALSE),
1✔
173
        USE_CHARACTER_MAPS(EXistOutputKeys.USE_CHARACTER_MAPS, Type.MAP_ITEM, Cardinality.ZERO_OR_ONE, Sequence.EMPTY_SEQUENCE),
1✔
174
        VERSION(OutputKeys.VERSION, Type.STRING, Cardinality.ZERO_OR_ONE, new StringValue("1.0"));
1✔
175

176
        private final String parameterName;
177
        private final int type;
178
        private final Cardinality cardinality;
179
        private final Sequence defaultValue;
180

181
        W3CParameterConvention(final String parameterName, final int type, final Cardinality cardinality, final Sequence defaultValue) {
1✔
182
            this.parameterName = parameterName;
1✔
183
            this.type = type;
1✔
184
            this.cardinality = cardinality;
1✔
185
            this.defaultValue = defaultValue;
1✔
186
        }
1✔
187

188
        @Override
189
        public String getParameterName() {
190
            return parameterName;
1✔
191
        }
192

193
        @Override
194
        public String getLocalParameterName() {
195
            return parameterName;
1✔
196
        }
197

198
        @Override
199
        public int getType() {
200
            return type;
1✔
201
        }
202

203
        @Override
204
        public Cardinality getCardinality() {
205
            return cardinality;
1✔
206
        }
207

208
        @Override
209
        public Sequence getDefaultValue() {
210
            return defaultValue;
1✔
211
        }
212
    }
213

214
    /**
215
     * for Exist xquery specific functions
216
     */
217
    public enum ExistParameterConvention implements ParameterConvention<QName> {
1✔
218
        OUTPUT_DOCTYPE(EXistOutputKeys.OUTPUT_DOCTYPE, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.FALSE),
1✔
219
        EXPAND_XINCLUDE(EXistOutputKeys.EXPAND_XINCLUDES, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.TRUE),
1✔
220
        PROCESS_XSL_PI(EXistOutputKeys.PROCESS_XSL_PI, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.TRUE),
1✔
221
        JSON_IGNORE_WHITE_SPACE_TEXT_NODES(EXistOutputKeys.JSON_IGNORE_WHITESPACE_TEXT_NODES, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.TRUE),
1✔
222
        HIGHLIGHT_MATCHES(EXistOutputKeys.HIGHLIGHT_MATCHES, Type.STRING, Cardinality.ZERO_OR_ONE, new StringValue("none")),
1✔
223
        JSONP(EXistOutputKeys.JSONP, Type.STRING, Cardinality.ZERO_OR_ONE, Sequence.EMPTY_SEQUENCE),
1✔
224
        ADD_EXIST_ID(EXistOutputKeys.ADD_EXIST_ID, Type.STRING, Cardinality.ZERO_OR_ONE, new StringValue("none")),
1✔
225
        // default of 4 corresponds to the existing eXist default, although 3 is in the spec
1✔
226
        INDENT_SPACES("indent-spaces", Type.INTEGER, Cardinality.ZERO_OR_ONE, new IntegerValue(4)),
1✔
227
        INSERT_FINAL_NEWLINE(EXistOutputKeys.INSERT_FINAL_NEWLINE, Type.BOOLEAN, Cardinality.ZERO_OR_ONE, BooleanValue.FALSE);
1✔
228

229

230
        private final QName parameterName;
231
        private final int type;
232
        private final Cardinality cardinality;
233
        private final Sequence defaultValue;
234

235
        ExistParameterConvention(final String parameterName, final int type, final Cardinality cardinality, final Sequence defaultValue) {
1✔
236
            this.parameterName = new QName(parameterName,Namespaces.EXIST_NS,Namespaces.EXIST_NS_PREFIX);
1✔
237
            this.type = type;
1✔
238
            this.cardinality = cardinality;
1✔
239
            this.defaultValue = defaultValue;
1✔
240
        }
1✔
241

242
        @Override
243
        public QName getParameterName() {
244
            return parameterName;
1✔
245
        }
246

247
        @Override
248
        public String getLocalParameterName() {
249
            return parameterName.getLocalPart();
1✔
250
        }
251

252
        @Override
253
        public int getType() {
254
            return type;
1✔
255
        }
256

257
        @Override
258
        public Cardinality getCardinality() {
259
            return cardinality;
1✔
260
        }
261

262
        @Override
263
        public Sequence getDefaultValue() {
264
            return defaultValue;
1✔
265
        }
266
    }
267

268
    /**
269
     * Parse output:serialization-parameters XML fragment into serialization
270
     * properties as defined by the fn:serialize function.
271
     *
272
     * @param parent     the parent expression calling this method
273
     * @param parameters root node of the XML fragment
274
     * @param serializationProperties parameters are added to the given properties
275
     * @throws XPathException in case of dynamic error
276
     */
277
    public static void getSerializationOptions(final Expression parent, final NodeValue parameters, final Properties serializationProperties) throws XPathException {
278
        try {
279
            final Properties propertiesInXML = new Properties();
1✔
280
            final XMLStreamReader reader = parent.getContext().getXMLStreamReader(parameters);
1✔
281
            while (reader.hasNext()) {
1!
282
                /* advance to the first starting element (root node) of the options */
283
                final int status = reader.next();
1✔
284
                if (status == XMLStreamConstants.START_ELEMENT) break;
1✔
285
            }
286

287
            if (!Namespaces.XSLT_XQUERY_SERIALIZATION_NS.equals(reader.getNamespaceURI())) {
1!
288
                throw new XPathException(parent, FnModule.SENR0001, "serialization parameter elements should be in the output namespace");
×
289
            }
290

291
            final int thisLevel = ((NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID)).getTreeLevel();
1✔
292

293
            while (reader.hasNext()) {
1!
294
                final int status = reader.next();
1✔
295
                if (status == XMLStreamConstants.START_ELEMENT) {
1✔
296
                    readStartElement(parent, reader, propertiesInXML);
1✔
297
                } else if (status == XMLStreamConstants.END_ELEMENT && readEndElementLevel(reader) == thisLevel) {
1!
298
                    // finished `optRoot` element ? exit-while
299
                    break;
1✔
300
                }
301
            }
302
            
303
            // Update properties with all the new ones
304
            serializationProperties.putAll(propertiesInXML);
1✔
305
            
306
        } catch (final XMLStreamException | IOException e) {
1✔
307
            throw new XPathException(parent, ErrorCodes.EXXQDY0001, e.getMessage());
×
308
        }
309
    }
1✔
310

311
    private static void readStartElement(final Expression parent, final XMLStreamReader reader, final Properties properties) throws XPathException, XMLStreamException {
312

313
        final javax.xml.namespace.QName key = reader.getName();
1✔
314
        final String local = key.getLocalPart();
1✔
315
        final String prefix = key.getPrefix();
1✔
316
        if (properties.containsKey(local)) {
1!
317
            throw new XPathException(parent, FnModule.SEPM0019, "serialization parameter specified twice: " + key);
×
318
        }
319
        if (prefix.equals(OUTPUT_NAMESPACE) && !W3CParameterConventionKeys.contains(local)) {
1✔
320
            throw new XPathException(ErrorCodes.SEPM0017, "serialization parameter not recognized: " + key);
1✔
321
        }
322

323
        readSerializationProperty(reader, local, properties);
1✔
324
    }
1✔
325

326
    private static int readEndElementLevel(final XMLStreamReader reader) {
327
        final NodeId otherId = (NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID);
1✔
328
        return otherId.getTreeLevel();
1✔
329
    }
330

331
    public static void setCharacterMap(final Properties serializationProperties, final Int2ObjectMap<String> characterMap) {
332
        serializationProperties.put(EXistOutputKeys.USE_CHARACTER_MAPS, characterMap);
1✔
333
    }
1✔
334

335
    public static Int2ObjectMap<String> getCharacterMap(final Properties serializationProperties) {
336
        return (Int2ObjectMap<String>) serializationProperties.get(EXistOutputKeys.USE_CHARACTER_MAPS);
1✔
337
    }
338

339
    private static void readSerializationProperty(final XMLStreamReader reader, final String key, final Properties serializationProperties) throws XPathException, XMLStreamException {
340

341
        final int attributeCount = reader.getAttributeCount();
1✔
342
        if (W3CParameterConvention.USE_CHARACTER_MAPS.parameterName.equals(key)) {
1✔
343
            if (attributeCount > 0) {
1!
344
                throw new XPathException(ErrorCodes.SEPM0017, MSG_NON_VALUE_ATTRIBUTE + ": " + key);
×
345
            }
346
            final Int2ObjectMap<String> characterMap = readUseCharacterMaps(reader);
1✔
347
            setCharacterMap(serializationProperties, characterMap);
1✔
348
        } else {
1✔
349
            String value = reader.getAttributeValue(XMLConstants.NULL_NS_URI, "value");
1✔
350
            if (value == null) {
1✔
351
                if (attributeCount > 0) {
1!
352
                    throw new XPathException(ErrorCodes.SEPM0017, MSG_NON_VALUE_ATTRIBUTE + ": " + key);
×
353
                }
354
                // backwards compatibility: use element text as value
355
                value = reader.getElementText();
1✔
356
            }
357
            if (attributeCount > 1) {
1✔
358
                throw new XPathException(ErrorCodes.SEPM0017, MSG_NON_VALUE_ATTRIBUTE + ": " + key);
1✔
359
            }
360

361
            setProperty(key, value, serializationProperties, reader.getNamespaceContext()::getNamespaceURI);
1✔
362
        }
363
    }
1✔
364

365
    private static Int2ObjectMap<String> readUseCharacterMaps(final XMLStreamReader reader) throws XMLStreamException, XPathException {
366
        if (reader.getAttributeCount() > 0) {
1!
367
            throw new XPathException(ErrorCodes.SEPM0017, EXistOutputKeys.USE_CHARACTER_MAPS + " element has attributes. It should contain only character-map children");
×
368
        }
369

370
        final Int2ObjectMap<String> characterMap = new Int2ObjectOpenHashMap(Hash.DEFAULT_INITIAL_SIZE, Hash.FAST_LOAD_FACTOR);
1✔
371
        int depth = 0;
1✔
372
        while (reader.hasNext()) {
1!
373
            /* advance to the next child element, or the end, of the use-character-maps element */
374
            final int status = reader.next();
1✔
375
            if (status == XMLStreamConstants.START_ELEMENT) {
1✔
376
                depth += 1;
1✔
377
                readCharacterMap(reader, characterMap);
1✔
378
            } else if (status == XMLStreamConstants.END_ELEMENT) {
1!
379
                if (depth == 0) return characterMap;
1✔
380
                depth -= 1;
1✔
381
            }
382
        }
383
        return characterMap;
×
384
    }
385

386
    /**
387
     * Read a single map element (k --> s) of a character map
388
     * @param reader which we are reading the input element from
389
     * @param characterMap to add the element to
390
     *
391
     * @throws XPathException if the element has a bad prefix, or unrecognized attributes
392
     */
393
    private static void readCharacterMap(final XMLStreamReader reader, final Int2ObjectMap<String> characterMap) throws XPathException {
394

395
        final javax.xml.namespace.QName qName = reader.getName();
1✔
396
        if (!qName.getPrefix().equals(OUTPUT_NAMESPACE)) {
1!
397
            throw new XPathException(ErrorCodes.SEPM0017, EXistOutputKeys.USE_CHARACTER_MAPS + " element with unexpected prefix: " + qName);
×
398
        }
399
        if (qName.getLocalPart().equals(CHARACTER_MAP_ELEMENT_KEY)) {
1!
400
            if (reader.getAttributeCount() > 2) {
1!
401
                throw new XPathException(ErrorCodes.SEPM0017, EXistOutputKeys.USE_CHARACTER_MAPS + " element has unexpected attributes");
×
402
            }
403
            final String character = readCharacterMapAttribute(reader, CHARACTER_ATTR_KEY);
1✔
404
            if (character.length() != 1) {
1!
405
                throw new XPathException(ErrorCodes.SEPM0017,
×
406
                        EXistOutputKeys.USE_CHARACTER_MAPS + " element character must be a single character string, was: " + character);
×
407
            }
408

409
            final int mapKey = character.charAt(0);
1✔
410
            if (characterMap.containsKey(mapKey)) {
1!
411
                throw new XPathException(ErrorCodes.SEPM0018, "Duplicate character map entry for key: " + character);
×
412
            }
413

414
            final String mapString = readCharacterMapAttribute(reader, MAP_STRING_ATTR_KEY);
1✔
415
            characterMap.put(mapKey, mapString);
1✔
416
        } else {
1✔
417
            throw new XPathException(ErrorCodes.SEPM0017, EXistOutputKeys.USE_CHARACTER_MAPS + " element must be character-map, was: " + qName);
×
418
        }
419
    }
1✔
420

421
    private static String readCharacterMapAttribute(final XMLStreamReader reader, final String key) throws XPathException {
422
        final String value = reader.getAttributeValue(XMLConstants.NULL_NS_URI, key);
1✔
423
        if (isNullOrEmpty(value)) {
1!
424
            throw new XPathException(ErrorCodes.SEPM0017, "Bad character-map element missing: " + key + " attribute");
×
425
        }
426
        return value;
1✔
427
    }
428

429
    public static void setProperty(final String key, final String value, final Properties properties,
430
                                   final Function<String, String> prefixToNs) {
431
        final ParameterConvention<String> parameterConvention = W3C_PARAMETER_CONVENTIONS_BY_NAME.get(key);
1✔
432
        if (parameterConvention == null || parameterConvention.getType() != Type.QNAME) {
1✔
433
            properties.setProperty(key, value);
1✔
434
        } else {
1✔
435
            final StringBuilder qnamesValue = new StringBuilder();
1✔
436
            final String[] qnameStrs = value.split("\\s");
1✔
437
            for (final String qnameStr : qnameStrs) {
1✔
438
                if (qnamesValue.length() > 0) {
1!
439
                    //separate entries with a space
440
                    qnamesValue.append(' ');
×
441
                }
442

443
                final String[] prefixAndLocal = qnameStr.split(":");
1✔
444
                if (prefixAndLocal.length == 1) {
1!
445
                    qnamesValue.append("{}").append(prefixAndLocal[0]);
1✔
446
                } else if (prefixAndLocal.length == 2) {
1!
447
                    final String prefix = prefixAndLocal[0];
×
448
                    final String ns = prefixToNs.apply(prefix);
×
449
                    qnamesValue.append('{').append(ns).append('}').append(prefixAndLocal[1]);
×
450
                }
451
            }
452

453
            properties.setProperty(key, qnamesValue.toString());
1✔
454
        }
455
    }
1✔
456

457
    public static Properties getSerializationOptions(final Expression parent, final AbstractMapType entries) throws XPathException {
458
        try {
459
            final Properties properties = new Properties();
1✔
460

461
            for (final W3CParameterConvention w3cParameterConvention : W3CParameterConvention.values()) {
1✔
462
                final Sequence parameterValue = getParameterValue(parent, entries, w3cParameterConvention,
1✔
463
                        new StringValue(w3cParameterConvention.getParameterName()));
1✔
464
                setPropertyForMap(properties, w3cParameterConvention, parameterValue);
1✔
465
            }
466

467
            for (final ExistParameterConvention existParameterConvention : ExistParameterConvention.values()) {
1✔
468
                final Sequence parameterValue = getParameterValue(parent, entries, existParameterConvention,
1✔
469
                        new QNameValue(null, existParameterConvention.getParameterName()));
1✔
470
                setPropertyForMap(properties, existParameterConvention, parameterValue);
1✔
471
            }
472

473
            return properties;
1✔
474
        } catch (final UnsupportedOperationException e) {
×
475
            throw new XPathException(parent, FnModule.SENR0001, e.getMessage());
×
476
        }
477
    }
478

479
    private static Sequence getParameterValue(final Expression parent, final AbstractMapType entries, final ParameterConvention<?> parameterConvention, final AtomicValue parameterConventionEntryKey)
480
            throws XPathException {
481
        final Sequence providedParameterValue = entries.get(parameterConventionEntryKey);
1✔
482

483
        // should we use the default value
484
        if (providedParameterValue == null || providedParameterValue.isEmpty() || (
1!
485
                parameterConvention.getType() == Type.STRING && isEmptyStringValue(providedParameterValue) &&
1✔
486
                        // allow empty separator #4704
487
                        parameterConvention.getParameterName() != EXistOutputKeys.ITEM_SEPARATOR
1!
488
        )
489
        ) {
490
            // use default value
491

492
            if (W3CParameterConvention.MEDIA_TYPE == parameterConvention) {
1✔
493
                // the default value of MEDIA_TYPE is dependent on the METHOD
494
                return getDefaultMediaType(entries.get(new StringValue(W3CParameterConvention.METHOD.getParameterName())));
1✔
495

496
            } else {
497
                return parameterConvention.getDefaultValue();
1✔
498
            }
499

500
        } else {
501
            // use provided value
502

503
            if (checkTypes(parameterConvention, providedParameterValue)) {
1!
504
                return providedParameterValue;
1✔
505
            } else {
506
                throw new XPathException(parent, ErrorCodes.XPTY0004, "The supplied value is of the wrong type for the particular parameter: " + parameterConvention.getParameterName());
×
507
            }
508
        }
509
    }
510

511
    private static Sequence getDefaultMediaType(final Sequence providedMethod) throws XPathException {
512
        final Sequence methodValue;
513

514
        // should we use the default method
515
        if (providedMethod == null || providedMethod.isEmpty()) {
1!
516
            //use default
517
            methodValue = W3CParameterConvention.METHOD.getDefaultValue();
1✔
518

519
        } else {
1✔
520
            //use provided
521
            methodValue = providedMethod;
1✔
522
        }
523

524
        final String method = methodValue.itemAt(0).getStringValue().toLowerCase();
1✔
525
        return switch (method) {
1!
526
            case "xml", "microxml" -> new StringValue("application/xml");
1✔
527
            case "xhtml" -> new StringValue("application/xhtml+xml");
×
528
            case "json" -> new StringValue("application/json");
1✔
529
            case "jsonp" -> new StringValue("application/javascript");
×
530
            case "html" -> new StringValue("text/html");
1✔
531
            case "adaptive", "text" -> new StringValue("text/plain");
1✔
532
            case "binary" -> new StringValue("application/octet-stream");
×
533
            default -> throw new UnsupportedOperationException("Unrecognised serialization method: " + method);
×
534
        };
535
    }
536

537
    /**
538
     * Checks that the types of the items in the sequence match the parameter convention.
539
     *
540
     * @param parameterConvention The parameter convention to check against
541
     * @param sequence The sequence to check the types of
542
     *
543
     * @return true if the types are suitable, false otherwise
544
     */
545
    private static boolean checkTypes(final ParameterConvention<?> parameterConvention, final Sequence sequence) throws XPathException {
546
        if (parameterConvention.getCardinality().isSuperCardinalityOrEqualOf(sequence.getCardinality())) {
1!
547
            final SequenceIterator iterator = sequence.iterate();
1✔
548
            while (iterator.hasNext()) {
1✔
549
                final Item item = iterator.nextItem();
1✔
550
                if (parameterConvention.getType() != item.getType()) {
1!
551
                    return false;
×
552
                }
553
            }
554

555
            return true;
1✔
556
        }
557

558
        return false;
×
559
    }
560

561
    private static void setPropertyForMap(final Properties properties, final ParameterConvention<?> parameterConvention, final Sequence parameterValue) throws XPathException {
562
        // ignore "absent","admit" i.e. "standalone" empty sequence
563
        if(parameterValue.isEmpty()) {
1✔
564
            return;
1✔
565
        }
566

567
        final String localParameterName = parameterConvention.getLocalParameterName();
1✔
568
        final String value;
569

570
        switch (parameterConvention.getType()) {
1!
571
            case Type.BOOLEAN:
572
                value = ((BooleanValue) parameterValue.itemAt(0)).getValue() ? "yes" : "no";
1✔
573
                properties.setProperty(localParameterName, value);
1✔
574
                break;
1✔
575
            case Type.STRING:
576
                value = ((StringValue)parameterValue.itemAt(0)).getStringValue();
1✔
577
                properties.setProperty(localParameterName, value);
1✔
578
                break;
1✔
579
            case Type.DECIMAL:
580
                value = parameterValue.itemAt(0).getStringValue();
1✔
581
                properties.setProperty(localParameterName, value);
1✔
582
                break;
1✔
583
            case Type.INTEGER:
584
                value = ((IntegerValue) parameterValue.itemAt(0)).getStringValue();
1✔
585
                properties.setProperty(localParameterName, value);
1✔
586
                break;
1✔
587
            case Type.QNAME:
588
                if (Cardinality._MANY.isSuperCardinalityOrEqualOf(parameterConvention.getCardinality())) {
1!
589
                    final SequenceIterator iterator = parameterValue.iterate();
×
590
                    while (iterator.hasNext()) {
×
591
                        final String existingValue = properties.getProperty(localParameterName);
×
592
                        final String nextValue = ((QNameValue) iterator.nextItem()).getQName().toURIQualifiedName();
×
593

594
                        if (existingValue == null || existingValue.isEmpty()) {
×
595
                            properties.setProperty(localParameterName, nextValue);
×
596
                        } else {
×
597
                            properties.setProperty(localParameterName, existingValue + " " + nextValue);
×
598
                        }
599
                    }
600
                } else {
×
601
                    value = ((QNameValue) parameterValue.itemAt(0)).getQName().toURIQualifiedName();
1✔
602
                    properties.setProperty(localParameterName, value);
1✔
603
                }
604
                break;
1✔
605
            case Type.MAP_ITEM:
606
                if (parameterConvention.getParameterName().equals(W3CParameterConvention.USE_CHARACTER_MAPS.parameterName)) {
1!
607
                    final Int2ObjectMap<String> characterMap = createCharacterMap((MapType) parameterValue, parameterConvention);
1✔
608
                    setCharacterMap(properties, characterMap);
1✔
609
                } else {
1✔
610
                    // There should not be any such parameter, other than use-character-maps
611
                    throw new UnsupportedOperationException(
×
612
                            "Not yet implemented support for the map serialization parameter: " + localParameterName);
×
613
                }
614
                break;
615
            default:
616
                throw new UnsupportedOperationException(
×
617
                        MSG_UNSUPPORTED_TYPE + Type.getTypeName(parameterConvention.getType()) + MSG_FOR_PARAMETER_VALUE + ": " + localParameterName);
×
618
        }
619
    }
1✔
620

621
    /**
622
     * Determines if the provided sequence contains a single empty string
623
     *
624
     * @param sequence The sequence to test
625
     *
626
     * @return if the sequence is a single empty string
627
     */
628
    private static boolean isEmptyStringValue(final Sequence sequence) {
629
        if(sequence != null && sequence.getItemCount() == 1) {
1!
630
            final Item firstItem = sequence.itemAt(0);
1✔
631
            return Type.STRING == firstItem.getType() && ((StringValue)firstItem).getStringValue().isEmpty();
1!
632
        }
633

634
        return false;
×
635
    }
636

637
    private static Int2ObjectMap<String> createCharacterMap(final MapType map, final ParameterConvention<?> parameterConvention) throws XPathException {
638

639
        final String localParameterName = parameterConvention.getLocalParameterName();
1✔
640
        final Int2ObjectMap<String> characterMap = new Int2ObjectOpenHashMap<>(Hash.DEFAULT_INITIAL_SIZE, Hash.FAST_LOAD_FACTOR);
1✔
641
        for (final IEntry<AtomicValue, Sequence> entry : map) {
1✔
642
            final AtomicValue key = entry.key();
1✔
643
            if (!Type.subTypeOf(key.getType(), Type.STRING)) {
1!
644
                throw new XPathException(ErrorCodes.XPTY0004,
×
645
                        MSG_UNSUPPORTED_TYPE + Type.getTypeName(key.getType()) + MSG_FOR_PARAMETER_VALUE + ": " + localParameterName +
×
646
                                ". Elements of the map for parameter value: " + localParameterName +
×
647
                                " must have keys of type " + Type.getTypeName(Type.STRING));
×
648
            }
649
            final Sequence sequence = entry.value();
1✔
650
            if (sequence.isEmpty()) {
1!
651
                throw new XPathException(ErrorCodes.XPTY0004, "Character map entries cannot be empty, " +
×
652
                        MSG_FOR_PARAMETER_VALUE + ": " + localParameterName);
×
653
            }
654
            final Item value = sequence.itemAt(0);
1✔
655
            if (!Type.subTypeOf(value.getType(), Type.STRING)) {
1!
656
                throw new XPathException(ErrorCodes.XPTY0004,
×
657
                        MSG_UNSUPPORTED_TYPE + Type.getTypeName(key.getType()) + MSG_FOR_PARAMETER_VALUE + ": " + localParameterName +
×
658
                                ". Elements of the map for parameter value: " + localParameterName +
×
659
                                " must have values of type " + Type.getTypeName(Type.STRING));
×
660
            }
661
            if (key.getStringValue().length() != 1) {
1!
662
                throw new XPathException(ErrorCodes.SEPM0017,
×
663
                        "Elements of the map for parameter value: " + localParameterName +
×
664
                                " must have keys which are strings composed of a single character");
665
            }
666
            characterMap.put(key.getStringValue().codePointAt(0), value.getStringValue());
1✔
667
        }
668
        return characterMap;
1✔
669
    }
670

671
}
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