Skip to content

Commit 38b5bcd

Browse files
committed
[feature] Added API serialization for XDM Map types
Closes #68
1 parent 1af9f85 commit 38b5bcd

File tree

4 files changed

+165
-2
lines changed

4 files changed

+165
-2
lines changed

exist-core/src/main/java/org/exist/storage/serializers/Serializer.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import javax.xml.transform.stream.StreamSource;
6868

6969
import com.evolvedbinary.j8fu.lazy.LazyVal;
70+
import io.lacuna.bifurcan.IEntry;
7071
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
7172
import org.apache.commons.io.output.StringBuilderWriter;
7273
import org.apache.logging.log4j.LogManager;
@@ -96,6 +97,8 @@
9697
import org.exist.xquery.XPathException;
9798
import org.exist.xquery.XQueryContext;
9899
import org.exist.xquery.functions.array.ArrayType;
100+
import org.exist.xquery.functions.map.MapType;
101+
import org.exist.xquery.value.AtomicValue;
99102
import org.exist.xquery.value.Item;
100103
import org.exist.xquery.value.NodeValue;
101104
import org.exist.xquery.value.Sequence;
@@ -176,6 +179,9 @@ public abstract class Serializer implements XMLReader {
176179
private static final QName ELEM_VALUE_QNAME = new QName("value", Namespaces.EXIST_NS, "exist");
177180
private static final QName ELEM_ARRAY_QNAME = new QName("array", Namespaces.EXIST_NS, "exist");
178181
private static final QName ELEM_SEQUENCE_QNAME = new QName("sequence", Namespaces.EXIST_NS, "exist");
182+
private static final QName ELEM_MAP_QNAME = new QName("map", Namespaces.EXIST_NS, "exist");
183+
private static final QName ELEM_ENTRY_QNAME = new QName("entry", Namespaces.EXIST_NS, "exist");
184+
private static final QName ELEM_KEY_QNAME = new QName("key", Namespaces.EXIST_NS, "exist");
179185

180186
// required for XQJ/typed information implementation
181187
// -----------------------------------------
@@ -1210,6 +1216,10 @@ private void itemToSAX(final Item item, final boolean typed, final boolean wrap)
12101216
} else {
12111217
if (item.getType() == Type.ARRAY_ITEM) {
12121218
serializeTypeArray((ArrayType) item, typed, wrap);
1219+
1220+
} else if (item.getType() == Type.MAP_ITEM) {
1221+
serializeTypeMap((MapType) item, typed, wrap);
1222+
12131223
} else {
12141224
serializeTypeAtomicValue(item, typed, wrap);
12151225
}
@@ -1261,6 +1271,49 @@ private void serializeTypeArray(final ArrayType arrayType, final boolean typed,
12611271
}
12621272
}
12631273

1274+
private void serializeTypeMap(final MapType mapType, final boolean typed, final boolean wrap) throws SAXException {
1275+
try {
1276+
if (typed) {
1277+
receiver.startElement(ELEM_MAP_QNAME, null);
1278+
}
1279+
1280+
for (final IEntry<AtomicValue, Sequence> mapEntry : mapType) {
1281+
if (typed) {
1282+
receiver.startElement(ELEM_ENTRY_QNAME, null);
1283+
}
1284+
1285+
if (typed) {
1286+
receiver.startElement(ELEM_KEY_QNAME, null);
1287+
}
1288+
itemToSAX(mapEntry.key(), typed, wrap);
1289+
if (typed) {
1290+
receiver.endElement(ELEM_KEY_QNAME);
1291+
}
1292+
1293+
if (typed) {
1294+
receiver.startElement(ELEM_SEQUENCE_QNAME, null);
1295+
}
1296+
for (final SequenceIterator itItem = mapEntry.value().iterate(); itItem.hasNext(); ) {
1297+
final Item item = itItem.nextItem();
1298+
itemToSAX(item, typed, wrap);
1299+
}
1300+
if (typed) {
1301+
receiver.endElement(ELEM_SEQUENCE_QNAME);
1302+
}
1303+
1304+
if (typed) {
1305+
receiver.endElement(ELEM_ENTRY_QNAME);
1306+
}
1307+
}
1308+
1309+
if (typed) {
1310+
receiver.endElement(ELEM_MAP_QNAME);
1311+
}
1312+
} catch (final XPathException e) {
1313+
throw new SAXException(e.getMessage(), e);
1314+
}
1315+
}
1316+
12641317
public void toReceiver(final NodeProxy p, final boolean highlightMatches) throws SAXException {
12651318
toReceiver(p, highlightMatches, true);
12661319
}

exist-core/src/main/xsd/rest-api.xsd

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
<xs:element ref="exist:document"/>
100100
<xs:element ref="exist:text"/>
101101
<xs:element ref="exist:array"/>
102+
<xs:element ref="exist:map"/>
102103
</xs:choice>
103104
</xs:sequence>
104105
<xs:attributeGroup ref="exist:queryAttrs"></xs:attributeGroup>
@@ -123,6 +124,7 @@
123124
<xs:element ref="exist:document"/>
124125
<xs:element ref="exist:text"/>
125126
<xs:element ref="exist:array"/>
127+
<xs:element ref="exist:map"/>
126128
</xs:choice>
127129
</xs:sequence>
128130
</xs:complexType>
@@ -160,6 +162,27 @@
160162
</xs:complexType>
161163
</xs:element>
162164

165+
<xs:element name="map">
166+
<xs:complexType>
167+
<xs:sequence>
168+
<xs:element name="entry" minOccurs="0" maxOccurs="unbounded">
169+
<xs:complexType>
170+
<xs:sequence>
171+
<xs:element name="key">
172+
<xs:complexType>
173+
<xs:sequence>
174+
<xs:element ref="exist:value"/>
175+
</xs:sequence>
176+
</xs:complexType>
177+
</xs:element>
178+
<xs:element ref="exist:sequence"/>
179+
</xs:sequence>
180+
</xs:complexType>
181+
</xs:element>
182+
</xs:sequence>
183+
</xs:complexType>
184+
</xs:element>
185+
163186
<xs:element name="collection">
164187
<xs:complexType>
165188
<xs:sequence>

exist-core/src/test/java/org/exist/storage/serializers/NativeSerializerTest.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
*/
2121
package org.exist.storage.serializers;
2222

23+
import com.evolvedbinary.j8fu.tuple.Tuple2;
24+
import io.lacuna.bifurcan.IEntry;
2325
import org.easymock.Capture;
2426
import org.exist.dom.memtree.MemTreeBuilder;
2527
import org.exist.util.Configuration;
2628
import org.exist.util.serializer.SAXSerializer;
2729
import org.exist.xquery.XPathException;
2830
import org.exist.xquery.XQueryContext;
2931
import org.exist.xquery.functions.array.ArrayType;
32+
import org.exist.xquery.functions.map.MapType;
3033
import org.exist.xquery.value.*;
3134
import org.junit.jupiter.params.ParameterizedTest;
3235
import org.junit.jupiter.params.provider.CsvSource;
@@ -40,6 +43,7 @@
4043
import java.util.List;
4144

4245
import static com.evolvedbinary.j8fu.function.FunctionE.identity;
46+
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
4347
import static org.easymock.EasyMock.*;
4448
import static org.exist.Namespaces.EXIST_NS;
4549
import static org.exist.Namespaces.EXIST_NS_PREFIX;
@@ -108,6 +112,25 @@ public void serializeArray(final Wrapped wrapped, final Typed typed) throws SAXE
108112
assertSerialize(sequence, wrapped == Wrapped.WRAPPED, typed == Typed.TYPED);
109113
}
110114

115+
@ParameterizedTest
116+
@CsvSource({"NOT_WRAPPED,NOT_TYPED", "WRAPPED,NOT_TYPED", "NOT_WRAPPED,TYPED", "WRAPPED,TYPED"})
117+
public void serializeMap(final Wrapped wrapped, final Typed typed) throws SAXException, XPathException, IOException {
118+
final XQueryContext mockContext = mock(XQueryContext.class);
119+
expect(mockContext.nextExpressionId()).andReturn(1).anyTimes();
120+
121+
replay(mockContext);
122+
123+
final Sequence sequence = new ValueSequence();
124+
final MapType map1 = new MapType(mockContext, null, List.of(Tuple(new StringValue("key1"), ValueSequence.of(identity(), new StringValue("hello"), new IntegerValue(42))), Tuple(new StringValue("key2"), ValueSequence.of(identity(), new StringValue("goodbye")))));
125+
sequence.add(map1);
126+
final MapType map2 = new MapType(mockContext, null, List.of(Tuple(new StringValue("key3"), ValueSequence.of(identity(), new StringValue("in the beginning"), new StringValue("but at the end"), new IntegerValue(42)))));
127+
sequence.add(map2);
128+
129+
verify(mockContext);
130+
131+
assertSerialize(sequence, wrapped == Wrapped.WRAPPED, typed == Typed.TYPED);
132+
}
133+
111134
@ParameterizedTest
112135
@CsvSource({"NOT_WRAPPED,NOT_TYPED", "WRAPPED,NOT_TYPED", "NOT_WRAPPED,TYPED", "WRAPPED,TYPED"})
113136
public void serializeMixed(final Wrapped wrapped, final Typed typed) throws SAXException, XPathException, IOException {
@@ -172,7 +195,16 @@ private static List<String> toStrings(final Sequence sequence) throws XPathExcep
172195
arrayStringBuilder.append(join(toStrings(arrayItem), ""));
173196
}
174197
strings.add(arrayStringBuilder.toString());
175-
} else {
198+
199+
} else if (item.getType() == Type.MAP_ITEM) {
200+
final StringBuilder mapStringBuilder = new StringBuilder();
201+
for (final IEntry<AtomicValue, Sequence> mapEntry : (MapType) item) {
202+
mapStringBuilder.append(mapEntry.key().getStringValue());
203+
mapStringBuilder.append(join(toStrings(mapEntry.value()), ""));
204+
}
205+
strings.add(mapStringBuilder.toString());
206+
207+
} else {
176208
strings.add(item.getStringValue());
177209
}
178210
}
@@ -194,8 +226,10 @@ private static List<String> type(final Sequence sequence, final boolean explicit
194226
final List<String> typed = new ArrayList<>(sequence.getItemCount());
195227
for (final SequenceIterator it = sequence.iterate(); it.hasNext();) {
196228
final Item item = it.nextItem();
229+
197230
if (item.getType() == Type.TEXT) {
198231
typed.add("<exist:text" + namespace + ">" + item.getStringValue() + "</exist:text>");
232+
199233
} else if (item.getType() == Type.ARRAY_ITEM) {
200234
final StringBuilder builder = new StringBuilder();
201235
builder.append("<exist:array").append(namespace).append('>');
@@ -206,13 +240,34 @@ private static List<String> type(final Sequence sequence, final boolean explicit
206240
}
207241
builder.append("</exist:array>");
208242
typed.add(builder.toString());
243+
244+
} else if (item.getType() == Type.MAP_ITEM) {
245+
final StringBuilder builder = new StringBuilder();
246+
builder.append("<exist:map").append(namespace).append('>');
247+
for (final IEntry<AtomicValue, Sequence> mapEntry : (MapType) item) {
248+
builder.append("<exist:entry>");
249+
builder.append("<exist:key>");
250+
builder.append(atomicValue(namespace, mapEntry.key()));
251+
builder.append("</exist:key>");
252+
builder.append("<exist:sequence>");
253+
builder.append(join(type(mapEntry.value(), explicitNamespace), ""));
254+
builder.append("</exist:sequence>");
255+
builder.append("</exist:entry>");
256+
}
257+
builder.append("</exist:map>");
258+
typed.add(builder.toString());
259+
209260
} else {
210-
typed.add("<exist:value" + namespace + " exist:type=\"" + Type.getTypeName(item.getType()) + "\">" + item.getStringValue() + "</exist:value>");
261+
typed.add(atomicValue(namespace, item));
211262
}
212263
}
213264
return typed;
214265
}
215266

267+
private static String atomicValue(final String namespace, final Item item) throws XPathException {
268+
return "<exist:value" + namespace + " exist:type=\"" + Type.getTypeName(item.getType()) + "\">" + item.getStringValue() + "</exist:value>";
269+
}
270+
216271
private static String wrap(final List<String> items) {
217272
final StringBuilder builder = new StringBuilder()
218273
.append("<exist:result xmlns:exist=\"http://exist.sourceforge.net/NS/exist\" exist:hits=\"")

schema/exist-rest-api.xsd

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
<xs:element ref="exist:document"/>
189189
<xs:element ref="exist:text"/>
190190
<xs:element ref="exist:array"/>
191+
<xs:element ref="exist:map"/>
191192
<xs:any processContents="lax" notNamespace="##targetNamespace http://exist-db.org/xquery/types/serialized"/>
192193
</xs:choice>
193194
</xs:choice>
@@ -295,6 +296,7 @@
295296
<xs:element ref="exist:document"/>
296297
<xs:element ref="exist:text"/>
297298
<xs:element ref="exist:array"/>
299+
<xs:element ref="exist:map"/>
298300
<xs:any processContents="lax" notNamespace="##targetNamespace http://exist-db.org/xquery/types/serialized"/>
299301
</xs:choice>
300302
</xs:sequence>
@@ -369,6 +371,36 @@
369371
</xs:complexType>
370372
</xs:element>
371373

374+
<xs:element name="map">
375+
<xs:annotation>
376+
<xs:documentation>Wrapper for an XDM Map.</xs:documentation>
377+
</xs:annotation>
378+
<xs:complexType>
379+
<xs:sequence>
380+
<xs:element name="entry" minOccurs="0" maxOccurs="unbounded">
381+
<xs:annotation>
382+
<xs:documentation>Can be used for XDM Map entries.</xs:documentation>
383+
</xs:annotation>
384+
<xs:complexType>
385+
<xs:sequence>
386+
<xs:element name="key">
387+
<xs:annotation>
388+
<xs:documentation>The key for the Map entry.</xs:documentation>
389+
</xs:annotation>
390+
<xs:complexType>
391+
<xs:sequence>
392+
<xs:element ref="exist:value"/>
393+
</xs:sequence>
394+
</xs:complexType>
395+
</xs:element>
396+
<xs:element ref="exist:sequence"/>
397+
</xs:sequence>
398+
</xs:complexType>
399+
</xs:element>
400+
</xs:sequence>
401+
</xs:complexType>
402+
</xs:element>
403+
372404
<xs:simpleType name="yes_no">
373405
<xs:restriction base="xs:token">
374406
<xs:enumeration value="yes"/>

0 commit comments

Comments
 (0)