Skip to content

Commit 6066dc0

Browse files
committed
feat: Add support for coercing null to empty string on input/output
1 parent 968cb2d commit 6066dc0

File tree

10 files changed

+374
-166
lines changed

10 files changed

+374
-166
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public class BaseJsonUrlOptions implements JsonUrlOptions {
4646
*/
4747
private boolean allowEmptyUnquotedValue;
4848

49+
/**
50+
* When read/writing, use empty string rather than null.
51+
*/
52+
private boolean coerceNullToEmptyString;
53+
4954
/**
5055
* When read/writing, use empty string rather than null.
5156
*/
@@ -121,6 +126,20 @@ public void setSkipNulls(boolean skipNulls) {
121126
this.skipNulls = skipNulls;
122127
}
123128

129+
@Override
130+
public boolean isCoerceNullToEmptyString() {
131+
return coerceNullToEmptyString;
132+
}
133+
134+
/**
135+
* Enable/disable the coerce-null-to-empty-string options.
136+
* @param coerce true or false
137+
* @see #isCoerceNullToEmptyString()
138+
*/
139+
public void setCoerceNullToEmptyString(boolean coerce) {
140+
this.coerceNullToEmptyString = coerce;
141+
}
142+
124143
/**
125144
* Use this method to enable implied string literals and common related
126145
* options.

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

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
import static org.jsonurl.CharUtil.IS_QUOTE;
2525
import static org.jsonurl.CharUtil.IS_STRUCTCHAR;
2626
import static org.jsonurl.CharUtil.hexDecode;
27+
import static org.jsonurl.JsonUrlOptions.isCoerceNullToEmptyString;
28+
import static org.jsonurl.JsonUrlOptions.isEmptyUnquotedKeyAllowed;
29+
import static org.jsonurl.JsonUrlOptions.isEmptyUnquotedValueAllowed;
30+
import static org.jsonurl.JsonUrlOptions.isImpliedStringLiterals;
2731
import static org.jsonurl.SyntaxException.Message.MSG_BAD_CHAR;
2832
import static org.jsonurl.SyntaxException.Message.MSG_BAD_PCT_ENC;
2933
import static org.jsonurl.SyntaxException.Message.MSG_BAD_QSTR;
@@ -95,7 +99,7 @@ private static int percentDecode(
9599

96100
return (ch1 << 4) | ch2;
97101
}
98-
102+
99103
/**
100104
* parse true, false, and null literals.
101105
*
@@ -309,14 +313,14 @@ static String literalToJavaString(
309313
JsonUrlOptions options) {
310314

311315
if (stop <= start) {
312-
if (JsonUrlOptions.isEmptyUnquotedKeyAllowed(options)) {
316+
if (isEmptyUnquotedKeyAllowed(options)) {
313317
return EMPTY_STRING;
314318
}
315319

316320
throw new SyntaxException(MSG_EXPECT_LITERAL, start);
317321
}
318322

319-
if (JsonUrlOptions.isImpliedStringLiterals(options)) {
323+
if (isImpliedStringLiterals(options)) {
320324
return string(buf, text, start, stop, false);
321325
}
322326

@@ -347,6 +351,33 @@ static String literalToJavaString(
347351
//
348352
return string(buf, text, start, stop, false);
349353
}
354+
355+
private static <V> V literalEmptyString(
356+
int start,
357+
ValueFactory<V,?,?,?,?,?,?,?,?,?> factory,
358+
JsonUrlOptions options) {
359+
360+
if (isEmptyUnquotedValueAllowed(options)) {
361+
return factory.getString(EMPTY_STRING);
362+
}
363+
364+
throw new SyntaxException(MSG_EXPECT_LITERAL, start);
365+
}
366+
367+
private static <V> V literalTrueFalseNull(
368+
V value,
369+
ValueFactory<V,?,?,?,?,?,?,?,?,?> factory,
370+
JsonUrlOptions options) {
371+
372+
boolean coerce = factory.isNull(value)
373+
&& isCoerceNullToEmptyString(options);
374+
375+
if (coerce) {
376+
return factory.getString(EMPTY_STRING);
377+
}
378+
379+
return value;
380+
}
350381

351382
/**
352383
* parse a literal value
@@ -375,14 +406,10 @@ static <V> V literal(
375406
JsonUrlOptions options) {
376407

377408
if (stop <= start) {
378-
if (JsonUrlOptions.isEmptyUnquotedValueAllowed(options)) {
379-
return factory.getString("");
380-
}
381-
382-
throw new SyntaxException(MSG_EXPECT_LITERAL, start);
409+
return literalEmptyString(start, factory, options);
383410
}
384411

385-
if (JsonUrlOptions.isImpliedStringLiterals(options)) {
412+
if (isImpliedStringLiterals(options)) {
386413
return factory.getString(
387414
string(buf, text, start, stop, false));
388415
}
@@ -397,7 +424,7 @@ static <V> V literal(
397424

398425
V ret = factory.getTrueFalseNull(text, start, stop);
399426
if (ret != null) {
400-
return ret;
427+
return literalTrueFalseNull(ret, factory, options);
401428
}
402429

403430
final NumberBuilder num = nbuilder == null
@@ -588,6 +615,32 @@ private static void encode(
588615
}
589616
}
590617
}
618+
619+
private static <T extends Appendable> boolean appendEmptyString(
620+
T dest,
621+
boolean isKey,
622+
JsonUrlOptions options) throws IOException {
623+
//
624+
// empty string
625+
//
626+
boolean emptyOK = isKey
627+
? isEmptyUnquotedKeyAllowed(options)
628+
: isEmptyUnquotedValueAllowed(options);
629+
630+
if (emptyOK) {
631+
return false;
632+
}
633+
634+
if (isImpliedStringLiterals(options)) {
635+
throw new IOException("implied strings: unexpected empty string");
636+
}
637+
638+
//
639+
// the empty string must be quoted
640+
//
641+
dest.append("''");
642+
return true;
643+
}
591644
}
592645

593646
private JsonUrl() {
@@ -761,8 +814,9 @@ public static <V> V parseLiteral(
761814
int stop = start + length;
762815

763816
final SyntaxException.Message errmsg =
764-
JsonUrlOptions.isEmptyUnquotedValueAllowed(options)
765-
? null : MSG_EXPECT_LITERAL;
817+
isEmptyUnquotedValueAllowed(options)
818+
? null
819+
: MSG_EXPECT_LITERAL;
766820

767821
parseLiteralLength(text, start, stop, errmsg);
768822

@@ -831,6 +885,7 @@ public static <V> V parseLiteral(
831885
ValueFactory<V,?,?,?,?,?,?,?,?,?> factory) {
832886
return parseLiteral(text, 0, text.length(), factory, null);
833887
}
888+
834889

835890
/**
836891
* Append the given CharSequence as a string literal.
@@ -839,7 +894,35 @@ public static <V> V parseLiteral(
839894
* @param dest destination
840895
* @param text source
841896
* @param start offset in source
842-
* @param end length of source
897+
* @param end offset in source
898+
* @return true if dest was modified
899+
*/
900+
public static <T extends Appendable> boolean appendLiteral(
901+
T dest,
902+
CharSequence text,
903+
int start,
904+
int end,
905+
boolean isKey) throws IOException {
906+
907+
return appendLiteral(
908+
dest,
909+
text,
910+
start,
911+
end,
912+
isKey,
913+
JsonUrlOptions.fromObject(dest));
914+
}
915+
916+
917+
/**
918+
* Append the given CharSequence as a string literal.
919+
*
920+
* @param <T> destination type
921+
* @param dest destination
922+
* @param text source
923+
* @param start offset in source
924+
* @param end offset in source
925+
* @param options a valid JsonUrlOptions or null
843926
* @return true if dest was modified
844927
*/
845928
public static <T extends Appendable> boolean appendLiteral(// NOPMD - CyclomaticComplexity
@@ -851,29 +934,10 @@ public static <T extends Appendable> boolean appendLiteral(// NOPMD - Cyclomatic
851934
JsonUrlOptions options) throws IOException {
852935

853936
if (end <= start) {
854-
//
855-
// empty string
856-
//
857-
boolean emptyOK = isKey
858-
? JsonUrlOptions.isEmptyUnquotedKeyAllowed(options)
859-
: JsonUrlOptions.isEmptyUnquotedValueAllowed(options);
860-
861-
if (emptyOK) {
862-
return false;
863-
}
864-
865-
if (JsonUrlOptions.isImpliedStringLiterals(options)) {
866-
throw new IOException("implied strings: unexpected empty string");
867-
}
868-
869-
//
870-
// the empty string must be quoted
871-
//
872-
dest.append("''");
873-
return true;
937+
return Encode.appendEmptyString(dest, isKey, options);
874938
}
875939

876-
if (JsonUrlOptions.isImpliedStringLiterals(options)) {
940+
if (isImpliedStringLiterals(options)) {
877941
Encode.encode(dest, text, start, end, false, true);
878942
return true;
879943
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,26 @@ static boolean isSkipNulls(JsonUrlOptions opt) {
129129
return opt != null && opt.isSkipNulls() || false;
130130
}
131131

132+
/**
133+
* Test if the coerce-null-to-empty-string option is enabled.
134+
* If this option is enabled then {@code null} values in the
135+
* input or output will be replaced with an empty string.
136+
* @return true if enabled
137+
*/
138+
default boolean isCoerceNullToEmptyString() {
139+
return false;
140+
}
141+
142+
/**
143+
* Test if the coerce-null-to-empty-string option is enabled,
144+
* supplying the default value if necessary.
145+
* @param opt a valid JsonUrlOptions or null
146+
* @see #isCoerceNullToEmptyString()
147+
*/
148+
static boolean isCoerceNullToEmptyString(JsonUrlOptions opt) {
149+
return opt != null && opt.isCoerceNullToEmptyString() || false;
150+
}
151+
132152
/**
133153
* Cast an object to JsonUrlOptions.
134154
* @param obj a valid object or null

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
public abstract class JsonUrlTextAppender<A extends Appendable, R> // NOPMD
3838
extends BaseJsonUrlOptions implements JsonTextBuilder<A, R>, Appendable {
3939

40+
/**
41+
* The empty string.
42+
*/
43+
private static final String EMPTY_STRING = "";
44+
4045
/**
4146
* Destination, provided in constructor.
4247
*/
@@ -96,11 +101,16 @@ public JsonUrlTextAppender<A,R> nameSeparator() throws IOException {
96101

97102
@Override
98103
public JsonUrlTextAppender<A,R> addNull() throws IOException {
99-
if (isImpliedStringLiterals()) {
104+
if (isCoerceNullToEmptyString()) {
105+
add(EMPTY_STRING);
106+
107+
} else if (isImpliedStringLiterals()) {
100108
throw new IOException("implied strings: unexpected null");
109+
110+
} else {
111+
out.append("null");
101112
}
102-
103-
out.append("null");
113+
104114
return this;
105115
}
106116

0 commit comments

Comments
 (0)