• 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

87.93
/exist-core/src/main/java/org/exist/xquery/functions/fn/transform/SerializationParameters.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.Tuple3;
27
import io.lacuna.bifurcan.IEntry;
28
import net.sf.saxon.om.StructuredQName;
29
import net.sf.saxon.serialize.CharacterMap;
30
import net.sf.saxon.serialize.CharacterMapIndex;
31
import net.sf.saxon.serialize.SerializationProperties;
32
import net.sf.saxon.z.IntHashMap;
33
import org.exist.util.CodePointString;
34
import org.exist.xquery.ErrorCodes;
35
import org.exist.xquery.XPathException;
36
import org.exist.xquery.functions.map.MapType;
37
import org.exist.xquery.value.*;
38

39
import java.util.*;
40
import java.util.function.BiFunction;
41

42
class SerializationParameters {
×
43

44
    private static class ParameterInfo {
45
        final String defaultValue;
46
        final boolean hasMany;
47
        final List<Integer> types;
48

49
        ParameterInfo(final String defaultValue, final boolean hasMany, final int... types) {
1✔
50

51
            this.defaultValue = defaultValue;
1✔
52
            this.hasMany = hasMany;
1✔
53
            this.types = new ArrayList<>();
1✔
54
            for (final int type : types) this.types.add(type);
1✔
55
        }
1✔
56

57
        ParameterInfo(final String defaultValue, final int... types) {
58
            this(defaultValue, false, types);
1✔
59
        }
1✔
60
    }
61

62
    static String asValue(final Sequence sequence, final String defaultValue) throws XPathException {
63
        if (sequence.isEmpty()) {
1!
64
            return defaultValue;
×
65
        }
66

67
        final StringBuilder sb = new StringBuilder();
1✔
68
        for (int i = 0; i < sequence.getItemCount(); i++) {
1✔
69
            final Item item = sequence.itemAt(i);
1✔
70
            if (Type.subTypeOf(item.getType(), Type.BOOLEAN)) {
1✔
71
                sb.append(' ').append(((BooleanValue) item).getValue() ? "yes" : "no");
1!
72
            } else if (Type.subTypeOf(item.getType(), Type.QNAME)) {
1!
73
                sb.append(' ').append(((QNameValue)item).getQName().toURIQualifiedName());
1✔
74
            } else {
1✔
75
                sb.append(' ').append(item.getStringValue());
×
76
            }
77
        }
78
        return sb.toString().trim();
1✔
79
    }
80

81
    static final Map<String, Param> keysAndTypes = new HashMap<>();
1✔
82
    static {
83
        for (final Param param : Param.values()) {
1✔
84
            keysAndTypes.put(param.key, param);
1✔
85
        }
86
    }
87

88
    static final String ABSENT = "absent";
89
    static final String NONE = "none";
90
    static final String YES = "yes";
91
    static final String NO = "no";
92

93
    static final String USE_CHARACTER_MAPS = "use-character-maps";
94

95
    enum Param {
1✔
96
        ALLOW_DUPLICATE_NAMES(Type.BOOLEAN, NO),
1✔
97
        BYTE_ORDER_MARK(Type.BOOLEAN, NO),
1✔
98
        CDATA_SECTION_ELEMENTS(Type.QNAME, "()", true),
1✔
99
        DOCTYPE_PUBLIC(Type.STRING, ABSENT),
1✔
100
        DOCTYPE_SYSTEM(Type.STRING, ABSENT),
1✔
101
        ENCODING(Type.STRING,"utf-8"),
1✔
102
        ESCAPE_URI_ATTRIBUTES(Type.BOOLEAN, YES),
1✔
103
        HTML_VERSION(Type.DECIMAL, "5"),
1✔
104
        INCLUDE_CONTENT_TYPE(Type.BOOLEAN, YES),
1✔
105
        INDENT(Type.BOOLEAN, NO),
1✔
106
        ITEM_SEPARATOR(Type.STRING, ABSENT),
1✔
107
        //JSON_NODE_OUTPUT_METHOD
1✔
108
        MEDIA_TYPE(Type.STRING, ""),
1✔
109
        METHOD(Type.STRING, "xml"),
1✔
110
        NORMALIZATION_FORM(Type.STRING, NONE),
1✔
111
        OMIT_XML_DECLARATION(Type.BOOLEAN, YES),
1✔
112
        STANDALONE(Type.BOOLEAN, "omit"),
1✔
113
        SUPPRESS_INDENTATION(Type.QNAME, "()", true),
1✔
114
        UNDECLARE_PREFIXES(Type.BOOLEAN, NO),
1✔
115
        USE_CHARACTER_MAPS(Type.MAP_ITEM, "map{}"),
1✔
116
        VERSION(Type.STRING, "1.0");
1✔
117

118
        private final ParameterInfo info;
119
        private final String key;
120

121
        Param(final int type, final String defaultValue, final boolean hasMany) {
1✔
122
            this.info = new ParameterInfo(defaultValue, hasMany, type);
1✔
123
            this.key = this.name().toLowerCase(Locale.ROOT).replaceAll("_","-");
1✔
124
        }
1✔
125

126
        Param(final int type, final String defaultValue) {
127
            this(type, defaultValue, false);
1✔
128
        }
1✔
129
    }
130

131
    private static String getKeyValue(final IEntry<AtomicValue, Sequence> entry,
132
                                      final BiFunction<ErrorCodes.ErrorCode, String, XPathException> errorBuilder) throws XPathException {
133
        if (!Type.subTypeOf(entry.key().getType(), Type.STRING)) {
1!
134
            throw errorBuilder.apply(ErrorCodes.XPTY0004,
×
135
                    "The key: " + entry.key() + " has type " + Type.getTypeName(entry.key().getType()) + " not a subtype of " + Type.getTypeName(Type.STRING));
×
136
        }
137
        return entry.key().getStringValue();
1✔
138
    }
139

140
    private static Sequence getEntryValue(final IEntry<AtomicValue, Sequence> entry,
141
                                          final ParameterInfo parameterInfo,
142
                                          final BiFunction<ErrorCodes.ErrorCode, String, XPathException> errorBuilder) throws XPathException {
143

144
        if (entry.value().isEmpty()) {
1!
145
            return Sequence.EMPTY_SEQUENCE;
×
146
        }
147

148
        final int count = entry.value().getItemCount();
1✔
149
        if (count == 1 || parameterInfo.hasMany) {
1!
150
            for (int i = 0; i < count; i++) {
1✔
151
                final Item item = entry.value().itemAt(i);
1✔
152
                boolean typeChecked = false;
1✔
153
                for (final int possibleType : parameterInfo.types) {
1✔
154
                    if (Type.subTypeOf(item.getType(), possibleType)) {
1!
155
                        typeChecked = true;
1✔
156
                    }
157
                }
158
                if (!typeChecked) {
1!
159
                    throw errorBuilder.apply(ErrorCodes.XPTY0004,
×
160
                            "The value: " + entry.key() + " has type " + Type.getTypeName(item.getType()) + " for item " + i + ", not any of the expected types");
×
161
                }
162
            }
163

164
            return entry.value();
1✔
165
        }
166

167
        throw errorBuilder.apply(ErrorCodes.XPTY0004,
×
168
                "The value: " + entry.key() + " has multiple values in the sequence, and is required to have none or one.");
×
169
    }
170

171
    private static Tuple3<String, Sequence, Param> getEntry(
172
            final IEntry<AtomicValue, Sequence> entry,
173
            final BiFunction<ErrorCodes.ErrorCode, String, XPathException> errorBuilder) throws XPathException {
174

175
        final String key = getKeyValue(entry, errorBuilder);
1✔
176
        final Param param = keysAndTypes.get(key);
1✔
177
        if (param == null) {
1!
178
            throw errorBuilder.apply(ErrorCodes.XPTY0004,
×
179
                    "The key: " + entry.key() + " is not a known serialization parameter.");
×
180
        }
181
        final Sequence value = getEntryValue(entry, param.info, errorBuilder);
1✔
182
        return new Tuple3<>(key, value, param);
1✔
183
    }
184

185
    static ParameterInfo characterMapEntryInfo = new ParameterInfo("", Type.STRING);
1✔
186

187
    static SerializationProperties getAsSerializationProperties(final MapType params,
188
                                                                final BiFunction<ErrorCodes.ErrorCode, String, XPathException> errorBuilder) throws XPathException {
189

190
        final SerializationProperties serializationProperties = new SerializationProperties(
1✔
191
                new Properties(),
1✔
192
                new CharacterMapIndex());
1✔
193

194
        for (final IEntry<AtomicValue, Sequence> param : params) {
1✔
195
            final Tuple3<String, Sequence, Param> entry = getEntry(param, errorBuilder);
1✔
196

197
            final String key = entry._1;
1✔
198

199
            if (entry._1.equals(Param.USE_CHARACTER_MAPS.key)) {
1✔
200
                final IntHashMap<String> characterMaps = new IntHashMap<>();
1✔
201

202
                final MapType mapType = (MapType) entry._2;
1✔
203
                for (final IEntry<AtomicValue, Sequence> mapEntry : mapType) {
1✔
204
                    final String mapKey = getKeyValue(mapEntry, errorBuilder);
1✔
205
                    final Sequence mapValue = getEntryValue(mapEntry, characterMapEntryInfo, errorBuilder);
1✔
206
                    final int codePoint = new CodePointString(mapKey).codePointAt(0);
1✔
207
                    characterMaps.put(codePoint, mapValue.getStringValue());
1✔
208
                }
209

210
                final CharacterMap characterMap = new CharacterMap(qNameCharacterMap, characterMaps);
1✔
211
                serializationProperties.getCharacterMapIndex().putCharacterMap(qNameCharacterMap, characterMap);
1✔
212
                serializationProperties.setProperty(Param.USE_CHARACTER_MAPS.key, qNameCharacterMap.getClarkName());
1✔
213
            } else {
1✔
214
                serializationProperties.setProperty(key, asValue(entry._2, entry._3.info.defaultValue));
1✔
215
            }
216
        }
217

218
        return serializationProperties;
1✔
219
    }
220

221
    static final StructuredQName qNameCharacterMap = new StructuredQName("", "http://www.exist-db.org", "fn-transform-charactermap");
1✔
222

223
    /**
224
     * {@link CharacterMap} doesn't provide a method of naming a combined map
225
     * <p></p>
226
     * We need our combined map to have a name to find it in a {@link CharacterMapIndex}
227
     */
228
    static class NamedCombinedCharacterMap extends CharacterMap {
229

230
        private final StructuredQName name;
231

232
        public NamedCombinedCharacterMap(final StructuredQName name, final Iterable<CharacterMap> list) {
233
            super(list);
1✔
234
            this.name = name;
1✔
235
        }
1✔
236

237
        @Override public StructuredQName getName() {
238
            return this.name;
1✔
239
        }
240
    }
241

242
    /**
243
     * Combine the serialization properties from a compiled stylesheet
244
     * with the serialization properties supplied as parameters to fn:transform
245
     * so that a serializer can be configured to serialize using the correct combination
246
     * of both sets of parameters.
247
     * <p></p>
248
     * There is an explicit step to combine character maps, which doesn't happen
249
     * using the raw {@link SerializationProperties#combineWith(SerializationProperties)} method.
250
     * <p></p>
251
     * This has to be done as a separate step because the fn:transform serialization properties
252
     * are read before compilation, during the initial phase of reading parameters.
253
     * We have to wait to compile the stylesheet before we can get its serialization properties.
254
     * <p></p>
255
     * That order could be changed for the fn:transform serialization property parameters..
256
     *
257
     * @param baseProperties properties and character map keys used when not overridden
258
     * @param overrideProperties properties and character map keys which take priority if present
259
     *
260
     * @return the carefully combined properties
261
     */
262
    static SerializationProperties combinePropertiesAndCharacterMaps(
263
            final SerializationProperties baseProperties,
264
            final SerializationProperties overrideProperties) {
265

266
        final SerializationProperties combinedProperties = overrideProperties.combineWith(baseProperties);
1✔
267

268
        final List<String> baseCharacterMapKeys = new ArrayList<>();
1✔
269
        final Optional<String[]> baseCharacterMapString = Optional.ofNullable(baseProperties.getProperty(USE_CHARACTER_MAPS)).map(s -> s.split(" "));
1✔
270
        if (baseCharacterMapString.isPresent()) {
1✔
271
            for (final String s : baseCharacterMapString.get()) {
1✔
272
                if (!s.isEmpty()) {
1✔
273
                    baseCharacterMapKeys.add(s);
1✔
274
                }
275
            }
276
        }
277

278
        final Optional<String> combinedCharacterMapKey = Optional.ofNullable(combinedProperties.getProperty(USE_CHARACTER_MAPS)).map(String::trim);
1✔
279
        if (combinedCharacterMapKey.isPresent()) {
1✔
280
            final List<CharacterMap> allMaps = new ArrayList<>();
1✔
281
            for (final String baseCharacterMapKey : baseCharacterMapKeys) {
1✔
282
                final CharacterMap baseCharacterMap = baseProperties.getCharacterMapIndex().getCharacterMap(new StructuredQName("", "", baseCharacterMapKey));
1✔
283
                allMaps.add(baseCharacterMap);
1✔
284
            }
285
            final CharacterMap combinedMap = combinedProperties.getCharacterMapIndex().getCharacterMap(qNameCharacterMap);
1✔
286
            allMaps.add(combinedMap);
1✔
287
            final CharacterMap repairedCombinedMap = new NamedCombinedCharacterMap(qNameCharacterMap, allMaps);
1✔
288
            combinedProperties.getCharacterMapIndex().putCharacterMap(
1✔
289
                    qNameCharacterMap,
1✔
290
                    repairedCombinedMap);
1✔
291
            combinedProperties.setProperty(USE_CHARACTER_MAPS, qNameCharacterMap.getClarkName());
1✔
292
        }
293

294
        return combinedProperties;
1✔
295
    }
296
}
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