Skip to content

Commit a423437

Browse files
committed
[feature] Add support for setting XQuery external variables of type map(*) via the eXist-db XML:DB API
See eXist-db/exist#4919
1 parent e182fad commit a423437

File tree

4 files changed

+573
-13
lines changed

4 files changed

+573
-13
lines changed

exist-core/src/main/java/org/exist/util/MapUtil.java

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434

3535
import com.evolvedbinary.j8fu.tuple.Tuple2;
3636

37-
import java.util.HashMap;
38-
import java.util.Map;
37+
import java.util.*;
38+
import java.util.regex.Matcher;
39+
import java.util.regex.Pattern;
3940

4041
/**
4142
* Simple utility functions for working with Java Maps.
@@ -78,4 +79,190 @@ static <K, V> Map<K,V> hashMap(final int initialCapacity, final Tuple2<K, V>...
7879
}
7980
return map;
8081
}
82+
83+
static Map<Object, Object> parseXdmMapStringToJavaMap(final String xdmMapString) {
84+
final XdmMapStringLexer lexer = new XdmMapStringLexer(xdmMapString);
85+
final XdmMapStringParser parser = new XdmMapStringParser(lexer);
86+
return parser.parseMap();
87+
}
88+
89+
class XdmMapStringLexer {
90+
private static final Pattern STRING_LITERAL = Pattern.compile("\"([^\"]*)\"");
91+
private static final Pattern LITERAL = Pattern.compile("[^\\s\",:{}()]+");
92+
private static final Pattern WHITESPACE = Pattern.compile("\\s*");
93+
94+
private final Matcher stringLiteralMatcher = STRING_LITERAL.matcher("");
95+
private final Matcher literalMatcher = LITERAL.matcher("");
96+
private final Matcher whitespaceMatcher = WHITESPACE.matcher("");
97+
98+
private final String input;
99+
private int pos = 0;
100+
101+
public XdmMapStringLexer(final String input) {
102+
this.input = input;
103+
}
104+
105+
public XdmMapStringToken nextToken() {
106+
skipWhitespace();
107+
if (pos >= input.length()) {
108+
return new XdmMapStringToken(XdmMapStringTokenType.EOF, "");
109+
}
110+
111+
final char ch = input.charAt(pos);
112+
switch (ch) {
113+
case '{':
114+
pos++;
115+
return new XdmMapStringToken(XdmMapStringTokenType.LBRACE, "{");
116+
117+
case '}':
118+
pos++;
119+
return new XdmMapStringToken(XdmMapStringTokenType.RBRACE, "}");
120+
121+
case ':':
122+
pos++;
123+
return new XdmMapStringToken(XdmMapStringTokenType.COLON, ":");
124+
125+
case ',':
126+
pos++;
127+
return new XdmMapStringToken(XdmMapStringTokenType.COMMA, ",");
128+
129+
case '(':
130+
pos++;
131+
return new XdmMapStringToken(XdmMapStringTokenType.LPAREN, "(");
132+
133+
case ')':
134+
pos++;
135+
return new XdmMapStringToken(XdmMapStringTokenType.RPAREN, ")");
136+
137+
case '"':
138+
stringLiteralMatcher.reset(input.substring(pos));
139+
if (stringLiteralMatcher.lookingAt()) {
140+
final String str = stringLiteralMatcher.group(1);
141+
pos += stringLiteralMatcher.end();
142+
return new XdmMapStringToken(XdmMapStringTokenType.STRING, str);
143+
}
144+
throw new IllegalArgumentException("Invalid string literal at position " + pos);
145+
146+
default:
147+
if (input.startsWith("map", pos)) {
148+
pos += 3;
149+
return new XdmMapStringToken(XdmMapStringTokenType.MAP, "map");
150+
}
151+
152+
literalMatcher.reset(input.substring(pos));
153+
if (literalMatcher.lookingAt()) {
154+
final String lit = literalMatcher.group();
155+
pos += literalMatcher.end();
156+
return new XdmMapStringToken(XdmMapStringTokenType.LITERAL, lit);
157+
}
158+
159+
throw new IllegalArgumentException("Unexpected character at position " + pos);
160+
}
161+
}
162+
163+
private void skipWhitespace() {
164+
whitespaceMatcher.reset(input.substring(pos));
165+
if (whitespaceMatcher.lookingAt()) {
166+
pos += whitespaceMatcher.end();
167+
}
168+
}
169+
}
170+
171+
class XdmMapStringParser {
172+
private final XdmMapStringLexer lexer;
173+
private XdmMapStringToken current;
174+
175+
public XdmMapStringParser(final XdmMapStringLexer lexer) {
176+
this.lexer = lexer;
177+
this.current = lexer.nextToken();
178+
}
179+
180+
private void consume(final XdmMapStringTokenType expected) {
181+
if (current.type != expected) {
182+
throw new IllegalArgumentException("Expected " + expected + " but found " + current.type);
183+
}
184+
current = lexer.nextToken();
185+
}
186+
187+
public Map<Object, Object> parseMap() {
188+
consume(XdmMapStringTokenType.MAP);
189+
consume(XdmMapStringTokenType.LBRACE);
190+
final Map<Object, Object> map = new LinkedHashMap<>();
191+
mapEntry(map);
192+
while (current.type == XdmMapStringTokenType.COMMA) {
193+
consume(XdmMapStringTokenType.COMMA);
194+
mapEntry(map);
195+
}
196+
consume(XdmMapStringTokenType.RBRACE);
197+
return map;
198+
}
199+
200+
private void mapEntry(final Map<Object, Object> map) {
201+
final String key = mapEntryKey();
202+
consume(XdmMapStringTokenType.COLON);
203+
final Object value = mapEntryValue();
204+
map.put(key, value);
205+
}
206+
207+
private String mapEntryKey() {
208+
if (current.type == XdmMapStringTokenType.STRING || current.type == XdmMapStringTokenType.LITERAL) {
209+
final String val = current.value;
210+
consume(current.type);
211+
return val;
212+
} else {
213+
throw new IllegalArgumentException("Expected STRING or LITERAL for key but got " + current.type);
214+
}
215+
}
216+
217+
private Object mapEntryValue() {
218+
if (current.type == XdmMapStringTokenType.LPAREN) {
219+
return mapEntryValueSequence();
220+
} else {
221+
return mapEntryValueItem();
222+
}
223+
}
224+
225+
private Object mapEntryValueItem() {
226+
if (current.type == XdmMapStringTokenType.STRING) {
227+
final String val = current.value;
228+
consume(current.type);
229+
return val;
230+
231+
} else if (current.type == XdmMapStringTokenType.LITERAL) {
232+
// NOTE(AR) at the moment we only support Integer literals
233+
final int val = Integer.parseInt(current.value);
234+
consume(current.type);
235+
return val;
236+
237+
} else {
238+
throw new IllegalArgumentException("Expected STRING or LITERAL for value item but got " + current.type);
239+
}
240+
}
241+
242+
private List<Object> mapEntryValueSequence() {
243+
consume(XdmMapStringTokenType.LPAREN);
244+
final List<Object> list = new ArrayList<>();
245+
list.add(mapEntryValueItem());
246+
while (current.type == XdmMapStringTokenType.COMMA) {
247+
consume(XdmMapStringTokenType.COMMA);
248+
list.add(mapEntryValueItem());
249+
}
250+
consume(XdmMapStringTokenType.RPAREN);
251+
return list;
252+
}
253+
}
254+
255+
class XdmMapStringToken {
256+
public final XdmMapStringTokenType type;
257+
public final String value;
258+
259+
public XdmMapStringToken(final XdmMapStringTokenType type, final String value) {
260+
this.type = type;
261+
this.value = value;
262+
}
263+
}
264+
265+
enum XdmMapStringTokenType {
266+
MAP, LBRACE, RBRACE, LPAREN, RPAREN, COLON, COMMA, STRING, LITERAL, EOF
267+
}
81268
}

exist-core/src/main/java/org/exist/xquery/functions/map/AbstractMapType.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,18 @@ public String toString() {
252252
buf.append(": ");
253253
final Sequence value = get((AtomicValue) key);
254254

255-
if(value != null && value.hasOne() && value instanceof StringValue) {
256-
buf.append('\"');
257-
}
258-
259-
buf.append(value);
260-
if(value != null && value.hasOne() && value instanceof StringValue) {
261-
buf.append('\"');
255+
if (value.hasOne()) {
256+
final Item item = value.itemAt(0);
257+
final boolean isString = Type.subTypeOf(item.getType(), Type.STRING);
258+
if (isString) {
259+
buf.append('"');
260+
}
261+
buf.append(item.toString());
262+
if (isString) {
263+
buf.append('"');
264+
}
265+
} else {
266+
buf.append(value);
262267
}
263268

264269
first = false;

exist-core/src/main/java/org/exist/xquery/value/ValueSequence.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,14 @@ public String toString() {
796796
result.append(", ");
797797
}
798798
moreThanOne = true;
799+
final boolean isString = Type.subTypeOf(next.getType(), Type.STRING);
800+
if (isString) {
801+
result.append('"');
802+
}
799803
result.append(next.toString());
804+
if (isString) {
805+
result.append('"');
806+
}
800807
}
801808
result.append(")");
802809
return result.toString();

0 commit comments

Comments
 (0)