Skip to content

Commit 609d658

Browse files
committed
feature: Add JsonUrlOption.NO_EMPTY_COMPOSITE
The commit includes support for both parsing and generating empty objects/arrays as outlined in section 2.9.5 of the spec.
1 parent fd08124 commit 609d658

File tree

12 files changed

+480
-41
lines changed

12 files changed

+480
-41
lines changed

module/jsonurl-core/src/main/java/org/jsonurl/JsonUrlOption.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,23 @@ public enum JsonUrlOption {
8080
/**
8181
* Address bar query string friendly.
8282
*/
83-
AQF;
83+
AQF,
84+
85+
/**
86+
* Distinguish between an empty object and empty array, and never
87+
* recognize the empty composite.
88+
*
89+
* <p>Empty array is back-to-back parens, i.e. {@code ()}. Empty object
90+
* is two parens with a single colon inside, i.e. {@code (:)}. Note that
91+
* this prevents the parser from recognizing {@code (:)} as an object with
92+
* a single member whose key and value is the unquoted empty string when
93+
* {@link #EMPTY_UNQUOTED_KEY} and {@link #EMPTY_UNQUOTED_VALUE} are also
94+
* both enabled.
95+
*
96+
* @see <a href="https://github.com/jsonurl/specification#295-empty-objects-and-arrays"
97+
* >JSON&#x2192;URL specification, section 2.9.5</a>
98+
*/
99+
NO_EMPTY_COMPOSITE;
84100

85101
/**
86102
* Create an empty set of options. This is just a convenience wrapper
@@ -234,4 +250,26 @@ public static final boolean optionAQF(
234250
return options != null && options.contains(AQF);
235251
}
236252

253+
/**
254+
* Test if the {@link #NO_EMPTY_COMPOSITE} option is enabled,
255+
* supplying the default value if necessary.
256+
* @param options a valid Set or {@code null}.
257+
*/
258+
public static final boolean optionNoEmptyComposite(
259+
Set<JsonUrlOption> options) {
260+
return options != null && options.contains(NO_EMPTY_COMPOSITE);
261+
}
262+
263+
/**
264+
* Test if the given options could result in ambigous input/output.
265+
* @param options a valid Set or {@code null}
266+
*/
267+
public static final boolean isAmbiguous(
268+
Set<JsonUrlOption> options) {
269+
270+
return options != null
271+
&& options.contains(NO_EMPTY_COMPOSITE)
272+
&& options.contains(EMPTY_UNQUOTED_KEY)
273+
&& options.contains(EMPTY_UNQUOTED_VALUE);
274+
}
237275
}

module/jsonurl-core/src/main/java/org/jsonurl/stream/AbstractGrammar.java

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919

2020
import static org.jsonurl.JsonUrlOption.optionEmptyUnquotedKey;
2121
import static org.jsonurl.JsonUrlOption.optionEmptyUnquotedValue;
22+
import static org.jsonurl.JsonUrlOption.optionNoEmptyComposite;
2223
import static org.jsonurl.JsonUrlOption.optionSkipNulls;
2324
import static org.jsonurl.JsonUrlOption.optionWfuComposite;
2425
import static org.jsonurl.LimitException.Message.MSG_LIMIT_MAX_PARSE_DEPTH;
2526
import static org.jsonurl.LimitException.Message.MSG_LIMIT_MAX_PARSE_VALUES;
2627
import static org.jsonurl.SyntaxException.Message.MSG_BAD_CHAR;
2728
import static org.jsonurl.SyntaxException.Message.MSG_EXPECT_LITERAL;
2829
import static org.jsonurl.SyntaxException.Message.MSG_EXPECT_OBJECT_VALUE;
29-
import static org.jsonurl.SyntaxException.Message.MSG_EXPECT_PAREN;
3030
import static org.jsonurl.SyntaxException.Message.MSG_EXPECT_STRUCT_CHAR;
3131
import static org.jsonurl.SyntaxException.Message.MSG_EXTRA_CHARS;
3232
import static org.jsonurl.SyntaxException.Message.MSG_NO_TEXT;
@@ -70,6 +70,7 @@ private enum State {
7070
IMPLIED_OBJECT,
7171
IN_OBJECT,
7272
OBJECT_HAVE_KEY,
73+
OBJECT_HAVE_KEY_SEPARATOR,
7374
OBJECT_AFTER_ELEMENT,
7475
END_STREAM,
7576
}
@@ -104,6 +105,11 @@ private enum State {
104105
*/
105106
protected static final char VALUE_SEPARATOR = ',';
106107

108+
/**
109+
* Generic internal parse error message.
110+
*/
111+
private static final String INTERNAL_PARSE_ERROR = "internal parse error";
112+
107113
/*
108114
* empty string.
109115
*
@@ -129,6 +135,7 @@ private enum State {
129135
* Buffered "next" event value.
130136
*/
131137
private JsonUrlEvent savedEventValue;
138+
//private Deque<JsonUrlEvent> savedEventStack = new LinkedList<>();
132139

133140
/**
134141
* Current parse/nesting depth.
@@ -189,15 +196,20 @@ protected abstract JsonUrlEvent readBufferedLiteral(
189196
boolean flag,
190197
boolean isKey);
191198

199+
/**
200+
* Test if the current buffered literal is empty.
201+
*/
202+
protected abstract boolean isEmptyBufferedLiteral(boolean flag);
203+
192204
/**
193205
* Read a literal and return its event.
194206
*/
195207
protected abstract JsonUrlEvent readLiteral(boolean isKey);
196208

197209
private JsonUrlEvent stateSavedEvent() {
198210
stateStack.pop();
199-
JsonUrlEvent ret = this.savedEventValue;
200-
this.savedEventValue = null; // NOPMD
211+
final JsonUrlEvent ret = savedEventValue;
212+
savedEventValue = null; // NOPMD
201213
return ret;
202214
}
203215

@@ -219,7 +231,8 @@ private JsonUrlEvent stateStart() {
219231
// non-structural character
220232
break;
221233
default:
222-
throw newSyntaxException(MSG_EXPECT_PAREN);
234+
// can't happen
235+
throw newParseException(INTERNAL_PARSE_ERROR);
223236
}
224237

225238
stateStack.set(0, State.END_STREAM);
@@ -242,7 +255,8 @@ private JsonUrlEvent stateStart() {
242255
checkResultType(ValueType.NUMBER);
243256
break;
244257
default:
245-
throw newParseException("interal parse error");
258+
// can't happen
259+
throw newParseException(INTERNAL_PARSE_ERROR);
246260
}
247261

248262
if (!eof()) {
@@ -251,9 +265,9 @@ private JsonUrlEvent stateStart() {
251265

252266
return ret;
253267
}
254-
255-
private JsonUrlEvent stateParenStructChar() {
256-
final int cval = nextStructChar(true);
268+
269+
private JsonUrlEvent stateParenParen() {
270+
int cval = nextStructChar(true);
257271

258272
switch (cval) {
259273
case BEGIN_COMPOSITE:
@@ -284,8 +298,6 @@ private JsonUrlEvent stateParenStructChar() {
284298
parseDepth--;
285299
stateStack.pop();
286300

287-
checkResultTypeIsComposite();
288-
289301
if (parseDepth == PARSE_DEPTH_DONE) {
290302
if (eof()) {
291303
stateStack.push(State.END_STREAM);
@@ -294,20 +306,28 @@ private JsonUrlEvent stateParenStructChar() {
294306
throw newSyntaxException(MSG_EXTRA_CHARS);
295307
}
296308
}
297-
return JsonUrlEvent.VALUE_EMPTY_COMPOSITE;
309+
310+
if (optionNoEmptyComposite(options())) {
311+
checkResultType(ValueType.ARRAY);
312+
savedEventValue = JsonUrlEvent.END_ARRAY;
313+
stateStack.push(State.SAVED_EVENT);
314+
return JsonUrlEvent.START_ARRAY;
315+
}
298316

317+
checkResultTypeIsComposite();
318+
return JsonUrlEvent.VALUE_EMPTY_COMPOSITE;
299319
default:
300320
return null;
301321
}
302322
}
303323

304324
@SuppressWarnings("PMD.CyclomaticComplexity")
305325
private JsonUrlEvent stateParen() {
306-
//
307-
// look for a structural character
308-
//
309-
JsonUrlEvent ret = stateParenStructChar();
326+
JsonUrlEvent ret = stateParenParen();
310327
if (ret != null) {
328+
//
329+
// found an open or close paren
330+
//
311331
return ret;
312332
}
313333

@@ -317,7 +337,7 @@ private JsonUrlEvent stateParen() {
317337
//
318338
boolean bufLitFlag = readAndBufferLiteral();
319339

320-
int sep = nextStructChar(true);
340+
final int sep = nextStructChar(true);
321341

322342
switch (sep) { // NOPMD - false positive
323343
case EOF:
@@ -345,10 +365,26 @@ private JsonUrlEvent stateParen() {
345365
//
346366
// key name for object
347367
//
348-
readBufferedLiteral(bufLitFlag, true);
349368
checkResultType(ValueType.OBJECT);
350-
stateStack.set(0, State.OBJECT_HAVE_KEY);
369+
skipChar();
370+
371+
if (isEmptyObject(sep, bufLitFlag)) {
372+
//
373+
// the empty object sequence (:)
374+
//
375+
// I'm not consuming the close paren because I don't have
376+
// enough context to manage `parseDepth`. But, I can set
377+
// my state to OBJECT_AFTER_ELEMENT in order to produce the
378+
// expected END_OBJECT event. This also handles eof(),
379+
// errors due to extra text, etc.
380+
//
381+
stateStack.set(0, State.OBJECT_AFTER_ELEMENT);
382+
return JsonUrlEvent.START_OBJECT;
383+
}
384+
385+
readBufferedLiteral(bufLitFlag, true);
351386
savedEventValue = JsonUrlEvent.KEY_NAME;
387+
stateStack.set(0, State.OBJECT_HAVE_KEY_SEPARATOR);
352388
stateStack.push(State.SAVED_EVENT);
353389
return JsonUrlEvent.START_OBJECT;
354390

@@ -406,8 +442,8 @@ private JsonUrlEvent stateObjectKeySeparator() {
406442
}
407443
}
408444

409-
private JsonUrlEvent stateNextValue(boolean isObject, State state) {
410-
if (isObject) {
445+
private JsonUrlEvent stateNextValue(boolean needKeySep, State state) {
446+
if (needKeySep) {
411447
JsonUrlEvent ret = stateObjectKeySeparator();
412448
if (ret != null) {
413449
return ret;
@@ -565,6 +601,11 @@ public JsonUrlEvent next() { // NOPMD - CyclomaticComplexity
565601
true, State.OBJECT_AFTER_ELEMENT));
566602
break;
567603

604+
case OBJECT_HAVE_KEY_SEPARATOR:
605+
ret = filterLiteral(stateNextValue(
606+
false, State.OBJECT_AFTER_ELEMENT));
607+
break;
608+
568609
case OBJECT_AFTER_ELEMENT:
569610
ret = stateAfterValue(
570611
State.IN_OBJECT,
@@ -693,6 +734,15 @@ private void checkWfuSeparator(boolean value) {
693734
consumeAmps();
694735
}
695736
}
737+
738+
private boolean isEmptyObject(int sep, boolean bufLitFlag) {
739+
final int end = nextStructChar(true);
740+
741+
return sep == NAME_SEPARATOR
742+
&& end == END_COMPOSITE
743+
&& isEmptyBufferedLiteral(bufLitFlag)
744+
&& optionNoEmptyComposite(options());
745+
}
696746

697747
/**
698748
* Return {@code KEY_NAME} if isKey is true.

module/jsonurl-core/src/main/java/org/jsonurl/stream/JsonUrlGrammar.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ protected JsonUrlEvent readBufferedLiteral(
230230
return readUnquotedLiteral(text, isKey);
231231
}
232232

233+
@Override
234+
protected boolean isEmptyBufferedLiteral(boolean flag) {
235+
return flag
236+
? this.decodedTextBuffer.length() == 0
237+
: this.rawTextBuffer.length() == 0;
238+
}
239+
233240
@Override
234241
protected JsonUrlEvent readLiteral(boolean isKey) {
235242
final int cval = nextChar();

module/jsonurl-core/src/main/java/org/jsonurl/stream/JsonUrlGrammarAQF.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ && optionCoerceNullToEmptyString(options())) {
270270
return keyEvent(isKey, JsonUrlEvent.VALUE_STRING);
271271
}
272272

273+
@Override
274+
protected boolean isEmptyBufferedLiteral(boolean flag) {
275+
return decodedTextBuffer.length() == 0;
276+
}
277+
273278
@Override
274279
protected JsonUrlEvent readLiteral(boolean isKey) {
275280
return readBufferedLiteral(readAndBufferLiteral(), isKey);

0 commit comments

Comments
 (0)