Skip to content

Commit 096a56e

Browse files
authored
Merge pull request #4703 from line-o/fix/4702
[hotfix] fn:serialize issues
2 parents 1d6ddde + 5685fdd commit 096a56e

File tree

5 files changed

+127
-33
lines changed

5 files changed

+127
-33
lines changed

exist-core/src/main/java/org/exist/xquery/functions/fn/FunSerialize.java

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.exist.storage.serializers.EXistOutputKeys;
2929
import org.exist.util.serializer.XQuerySerializer;
3030
import org.exist.xquery.*;
31+
import org.exist.xquery.functions.array.ArrayType;
3132
import org.exist.xquery.functions.map.AbstractMapType;
3233
import org.exist.xquery.util.SerializerUtils;
3334
import org.exist.xquery.value.*;
@@ -82,13 +83,16 @@ public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathExce
8283
try(final StringWriter writer = new StringWriter()) {
8384
final XQuerySerializer xqSerializer = new XQuerySerializer(context.getBroker(), outputProperties, writer);
8485

85-
Sequence seq = args[0];
86+
final Sequence input = args[0];
8687
if (xqSerializer.normalize()) {
8788
final String itemSeparator = outputProperties.getProperty(EXistOutputKeys.ITEM_SEPARATOR, DEFAULT_ITEM_SEPARATOR);
88-
seq = normalize(this, context, seq, itemSeparator);
89+
final Sequence normalized = normalize(this, context, input, itemSeparator);
90+
xqSerializer.serialize(normalized);
91+
}
92+
else {
93+
xqSerializer.serialize(input);
8994
}
9095

91-
xqSerializer.serialize(seq);
9296
return new StringValue(this, writer.toString());
9397
} catch (final IOException | SAXException e) {
9498
throw new XPathException(this, FnModule.SENR0001, e.getMessage());
@@ -97,9 +101,9 @@ public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathExce
97101

98102
public static Properties getSerializationProperties(final Expression callingExpr, final Item parametersItem) throws XPathException {
99103
final Properties outputProperties;
100-
if(parametersItem.getType() == Type.MAP) {
104+
if (parametersItem.getType() == Type.MAP) {
101105
outputProperties = SerializerUtils.getSerializationOptions(callingExpr, (AbstractMapType) parametersItem);
102-
} else if(isSerializationParametersElement(parametersItem)) {
106+
} else if (isSerializationParametersElement(parametersItem)) {
103107
outputProperties = new Properties();
104108
SerializerUtils.getSerializationOptions(callingExpr, (NodeValue) parametersItem, outputProperties);
105109
} else {
@@ -141,48 +145,59 @@ private static boolean isSerializationParametersElement(final Item item) {
141145
* @throws XPathException in case of dynamic error.
142146
*/
143147
public static Sequence normalize(final Expression callingExpr, final XQueryContext context, final Sequence input, final String itemSeparator) throws XPathException {
144-
if (input.isEmpty())
145-
// "If the sequence that is input to serialization is empty, create a sequence S1 that consists of a zero-length string."
146-
{return StringValue.EMPTY_STRING;}
147-
final ValueSequence temp = new ValueSequence(input.getItemCount());
148+
// "If the sequence that is input to serialization is empty, create a sequence S1 that consists of a zero-length string."
149+
if (input.isEmpty()) {
150+
return StringValue.EMPTY_STRING;
151+
}
152+
// flatten arrays
153+
final ValueSequence step1 = new ValueSequence();
148154
for (final SequenceIterator i = input.iterate(); i.hasNext(); ) {
149155
final Item next = i.nextItem();
150-
if (Type.subTypeOf(next.getType(), Type.NODE)) {
151-
if (next.getType() == Type.ATTRIBUTE || next.getType() == Type.NAMESPACE || next.getType() == Type.FUNCTION_REFERENCE)
152-
{throw new XPathException(callingExpr, FnModule.SENR0001,
153-
"It is an error if an item in the sequence to serialize is an attribute node or a namespace node.");}
154-
if (itemSeparator != null && itemSeparator.length() > 0 && !temp.isEmpty()) {
155-
temp.add(new StringValue(itemSeparator + next.getStringValue()));
156-
} else {
157-
temp.add(next);
156+
if (next.getType() == Type.ARRAY) {
157+
final Sequence sequence = ArrayType.flatten(next);
158+
for (final SequenceIterator si = sequence.iterate(); si.hasNext(); ) {
159+
step1.add(si.nextItem());
160+
}
161+
} else {
162+
step1.add(next);
163+
}
164+
}
165+
166+
final ValueSequence step2 = new ValueSequence(step1.getItemCount());
167+
for (final SequenceIterator i = step1.iterate(); i.hasNext(); ) {
168+
final Item next = i.nextItem();
169+
final int itemType = next.getType();
170+
if (Type.subTypeOf(itemType, Type.NODE)) {
171+
if (itemType == Type.ATTRIBUTE || itemType == Type.NAMESPACE || itemType == Type.FUNCTION_REFERENCE) {
172+
throw new XPathException(callingExpr, FnModule.SENR0001,
173+
"It is an error if an item in the sequence to serialize is an attribute node or a namespace node.");
158174
}
175+
step2.add(next);
159176
} else {
160177
// atomic value
161-
Item last = null;
162-
if (!temp.isEmpty())
163-
{last = temp.itemAt(temp.getItemCount() - 1);}
164-
if (last != null && last.getType() == Type.STRING)
165-
// "For each subsequence of adjacent strings in S2, copy a single string to the new sequence
166-
// equal to the values of the strings in the subsequence concatenated in order, each separated
167-
// by a single space."
168-
{((StringValue)last).append((itemSeparator == null ? " " : itemSeparator) + next.getStringValue());}
169-
else
170-
// "For each item in S1, if the item is atomic, obtain the lexical representation of the item by
171-
// casting it to an xs:string and copy the string representation to the new sequence;"
172-
{temp.add(new StringValue(callingExpr, next.getStringValue()));}
178+
// "For each item in S1, if the item is atomic, obtain the lexical representation of the item by
179+
// casting it to an xs:string and copy the string representation to the new sequence;"
180+
final StringValue stringRepresentation = new StringValue(callingExpr, next.getStringValue());
181+
step2.add(stringRepresentation);
173182
}
174183
}
175184

176185
context.pushDocumentContext();
177186
try {
178187
final MemTreeBuilder builder = context.getDocumentBuilder();
179188
final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(callingExpr, builder, true);
180-
for (final SequenceIterator i = temp.iterate(); i.hasNext(); ) {
189+
final String safeItemSeparator = itemSeparator == null ? "" : itemSeparator;
190+
for (final SequenceIterator i = step2.iterate(); i.hasNext(); ) {
181191
final Item next = i.nextItem();
182192
if (Type.subTypeOf(next.getType(), Type.NODE)) {
183193
next.copyTo(context.getBroker(), receiver);
184194
} else {
185-
receiver.characters(next.getStringValue());
195+
final String stringValue = next.getStringValue();
196+
receiver.characters(stringValue);
197+
}
198+
// add itemSeparator if there is a next item
199+
if (i.hasNext()) {
200+
receiver.characters(safeItemSeparator);
186201
}
187202
}
188203
return (DocumentImpl)receiver.getDocument();

exist-core/src/main/java/org/exist/xquery/functions/util/Eval.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ private Sequence doEval(final XQueryContext evalContext, final Sequence contextS
404404

405405
final Sequence seq;
406406
if (xqSerializer.normalize()) {
407+
// TODO(JL): should this not be changed to DEFAULT_ITEM_SEPARATOR
407408
seq = FunSerialize.normalize(this, context, result, null);
408409
} else {
409410
seq = result;

exist-core/src/main/java/org/exist/xquery/util/SerializerUtils.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,12 @@ private static Sequence getParameterValue(final Expression parent, final Abstrac
454454
final Sequence providedParameterValue = entries.get(parameterConventionEntryKey);
455455

456456
// should we use the default value
457-
if (providedParameterValue == null || providedParameterValue.isEmpty() || (parameterConvention.getType() == Type.STRING && isEmptyStringValue(providedParameterValue))) {
457+
if (providedParameterValue == null || providedParameterValue.isEmpty() || (
458+
parameterConvention.getType() == Type.STRING && isEmptyStringValue(providedParameterValue) &&
459+
// allow empty separator #4704
460+
parameterConvention.getParameterName() != EXistOutputKeys.ITEM_SEPARATOR
461+
)
462+
) {
458463
// use default value
459464

460465
if (W3CParameterConvention.MEDIA_TYPE == parameterConvention) {

exist-core/src/test/java/xquery/xquery3/XQuery3Tests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
@XSuite.XSuiteFiles({
2929
"src/test/xquery/xquery3",
3030
"src/test/xquery/xquery3/transform",
31-
//Reminder - add an individual test like this - "src/test/xquery/xquery3/transform/<test-file>.xqm",
31+
// To add an individual test or only run a specific set of tests -
32+
// "src/test/xquery/xquery3/serialize.xql",
3233
})
3334
public class XQuery3Tests {
3435
}

exist-core/src/test/xquery/xquery3/serialize.xql

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,26 @@ function ser:serialize-atomic() {
224224
fn:serialize($nodes)
225225
};
226226

227+
(: test for https://github.com/eXist-db/exist/issues/4704 :)
228+
declare
229+
%test:assertEquals("aaabbb")
230+
function ser:serialize-atomic-empty-separator() {
231+
fn:serialize(("aaa", "bbb"), map {"item-separator": ""})
232+
};
233+
234+
(: test for https://github.com/eXist-db/exist/issues/4704 :)
235+
declare
236+
%test:assertEquals("aaabbb")
237+
function ser:serialize-atomic-empty-separator-xml-options() {
238+
fn:serialize(("aaa", "bbb"),
239+
<output:serialization-parameters
240+
xmlns:output="http://www.w3.org/2010/xslt-xquery-serialization">
241+
<output:item-separator value=""/>
242+
</output:serialization-parameters>
243+
)
244+
};
245+
246+
227247
declare
228248
%test:assertEquals("")
229249
function ser:serialize-empty-sequence() {
@@ -927,3 +947,55 @@ function ser:serialize-html-5-needs-escape-elements() {
927947
=> serialize($params)
928948
=> normalize-space()
929949
};
950+
951+
(: test for https://github.com/eXist-db/exist/issues/4702 :)
952+
declare
953+
%test:assertEquals("<a>foo</a> <b>bar</b>")
954+
function ser:sequence-of-nodes() {
955+
(<a>foo</a>, <b>bar</b>) => serialize()
956+
};
957+
958+
declare
959+
%test:assertEquals("[|]")
960+
function ser:sequence-skip-empty-text-node() {
961+
(<a>[</a>, <a>
962+
</a>, <a>]</a>)/text()
963+
=> serialize(map {"item-separator": "|"})
964+
};
965+
966+
declare
967+
%test:assertEquals("||")
968+
function ser:sequence-dont-skip-empty-string() {
969+
serialize(("", "", ""), map {"item-separator": "|"})
970+
};
971+
972+
declare
973+
%test:assertEquals("foo")
974+
function ser:skip-empty-no-separator() {
975+
(<a>foo</a>, <b></b>)/text()
976+
=> serialize(map{"item-separator": "!"})
977+
};
978+
979+
declare
980+
%test:assertEquals("")
981+
function ser:empty-array-serializes-to-empty-string() {
982+
serialize([])
983+
};
984+
985+
declare
986+
%test:assertEquals("")
987+
function ser:array-with-members-serializes-to-empty-string() {
988+
serialize(["", ()])
989+
};
990+
991+
declare
992+
%test:assertEquals("")
993+
function ser:sequence-of-empty-arrays-serializes-to-empty-string() {
994+
serialize(([],[]), map{"item-separator": "|"})
995+
};
996+
997+
declare
998+
%test:assertEquals("1|2")
999+
function ser:item-separator-applies-to-array-members() {
1000+
serialize(([1,2]), map{"item-separator":"|"})
1001+
};

0 commit comments

Comments
 (0)