Skip to content

Commit 3c7dd23

Browse files
authored
Implement #169 (escaping separators in JavaPropsMapper). (#332)
1 parent b4ec4f1 commit 3c7dd23

File tree

4 files changed

+230
-9
lines changed

4 files changed

+230
-9
lines changed

properties/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,33 @@ Currently existing configuration settings to use can be divide into three groups
234234
* `JavaPropsSchema.withPathSeparator(String)` to assign path separator (except if "" given, same as disabling)
235235
* `JavaPropsSchema.withoutPathSeparator()` to disable use of path logic; if so, only main-level properties are available, with exact property key as name
236236
237+
#### JavaPropsSchema.pathSeparatorEscapeChar
238+
239+
* Marker used to enable the JavaPropsSchema.pathSeparator to be included in key names.
240+
* Default value: '\0' (effectively disabling it).
241+
* Mutator methods
242+
* `JavaPropsSchema.withPathSeparatorEscapeChar(char)` to assign path separator escape char
243+
* Notes
244+
* The escape character is only used if the path separator is a single character.
245+
* The escape character is only used for escaping either the pathSeparator character
246+
or a sequence of escape characters immediately prior to the pathSeparator.
247+
* Any escape character may be used.
248+
* Backslash ('\\') is the most obvious character to use, but be aware that the JDK Properties
249+
loader has its own rules for escape processing (documented in the Javadoc for [Properties.load]
250+
(https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/Properties.html#load(java.io.Reader) )
251+
) that will remove ALL duplicated backslash characters (and also carry out other escape handling)
252+
before the JavaPropsMapper gets to see them.
253+
* Examples
254+
* Given a pathSeparator of "." and an escape char of '#' then
255+
* a#.b
256+
produces a segment called "a.b"
257+
* a##.b
258+
produces a segment called "a#" with a child called "b"
259+
* a###.b
260+
produces a segment called "a#.b"
261+
* a#b
262+
produces a segment called "a#b" - the escape processing is only used immediately prior to the path separator.
263+
237264
### JavaPropsSchema: array representation
238265
239266
#### JavaPropsSchema.firstArrayOffset

properties/src/main/java/com/fasterxml/jackson/dataformat/javaprop/JavaPropsSchema.java

+68
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,32 @@ public class JavaPropsSchema
6363
*/
6464
protected String _pathSeparator = ".";
6565

66+
/**
67+
* Default escape character to use for single character path separators
68+
* , enabling the pathSeparator to be included in a segment.
69+
* Note that this is only used if the path separator is a single character.
70+
*
71+
* The default value is NULL ('\0') which effectively disables escape processing.
72+
*
73+
* The escape character is only used for escaping either the pathSeparator character
74+
* or a sequence of escape characters immediately prior to the pathSeparator.
75+
* i.e., if the pathSeparator is "." and the escape char is '#' then "a#.b"
76+
* produces a segment called "a.b", but "a##.b" produces a segment called "a#"
77+
* with a child called "b" and "a###.b" produces a segment called "a#.b".
78+
* Finally, "a#b" produces a segment called "a#b" - the escape processing is only used
79+
* immediately prior to the path separator.
80+
*
81+
* Any escape character may be used.
82+
* Backslash ('\\') is the most obvious candidate but be aware that the JDK Properties
83+
* loader has its own rules for escape processing (documented in the Javadoc for
84+
* <a href="https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/Properties.html#load(java.io.Reader)">Properties.load</a>
85+
* ) that will remove ALL duplicated backslash characters (and also carry out
86+
* other escape handling) before the JavaPropsMapper gets to see them.
87+
*
88+
* @since 2.14
89+
*/
90+
protected char _pathSeparatorEscapeChar = '\0';
91+
6692
/**
6793
* Default start marker for index access, if any; empty String may be used
6894
* to indicate no marker-based index detection should be made.
@@ -158,6 +184,7 @@ public JavaPropsSchema() { }
158184
public JavaPropsSchema(JavaPropsSchema base) {
159185
_firstArrayOffset = base._firstArrayOffset;
160186
_pathSeparator = base._pathSeparator;
187+
_pathSeparatorEscapeChar = base._pathSeparatorEscapeChar;
161188
_indexMarker = base._indexMarker;
162189
_parseSimpleIndexes = base._parseSimpleIndexes;
163190
_writeIndexUsingMarkers = base._writeIndexUsingMarkers;
@@ -211,6 +238,40 @@ public JavaPropsSchema withPathSeparator(String v) {
211238
return s;
212239
}
213240

241+
/**
242+
* Mutant factory method for constructing a new instance with
243+
* a different escape character to use for single character path separators
244+
* , enabling the pathSeparator to be included in a segment.
245+
* Note that this is only used if the path separator is a single character.
246+
*
247+
* The default value is NULL ('\0') which effectively disables escape processing.
248+
*
249+
* The escape character is only used for escaping either the pathSeparator character
250+
* or a sequence of escape characters immediately prior to the pathSeparator.
251+
* i.e., if the pathSeparator is "." and the escape char is '#' then "a#.b"
252+
* produces a segment called "a.b", but "a##.b" produces a segment called "a#"
253+
* with a child called "b" and "a###.b" produces a segment called "a#.b".
254+
* Finally, "a#b" produces a segment called "a#b" - the escape processing is only used
255+
* immediately prior to the path separator.
256+
*
257+
* Any escape character may be used.
258+
* Backslash ('\\') is the most obvious candidate but be aware that the JDK Properties
259+
* loader has its own rules for escape processing (documented in the Javadoc for
260+
* <a href="https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/Properties.html#load(java.io.Reader)">Properties.load</a>
261+
* ) that will remove ALL duplicated backslash characters (and also carry out
262+
* other escape handling) before the JavaPropsMapper gets to see them.
263+
*
264+
* @since 2.14
265+
*/
266+
public JavaPropsSchema withPathSeparatorEscapeChar(char v) {
267+
if (_equals(v, _pathSeparator)) {
268+
return this;
269+
}
270+
JavaPropsSchema s = new JavaPropsSchema(this);
271+
s._pathSeparatorEscapeChar = v;
272+
return s;
273+
}
274+
214275
/**
215276
* Mutant factory method for constructing a new instance that
216277
* specifies that no "path splitting" is to be done: this is
@@ -396,6 +457,13 @@ public String pathSeparator() {
396457
return _pathSeparator;
397458
}
398459

460+
/**
461+
* @since 2.14
462+
*/
463+
public char pathSeparatorEscapeChar() {
464+
return _pathSeparatorEscapeChar;
465+
}
466+
399467
/**
400468
* @since 2.10
401469
*/

properties/src/main/java/com/fasterxml/jackson/dataformat/javaprop/util/JPropPathSplitter.java

+54-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ private static JPropPathSplitter pathOnlySplitter(JavaPropsSchema schema)
4747
}
4848
// otherwise it's still quite simple
4949
if (sep.length() == 1) {
50-
return new CharPathOnlySplitter(sep.charAt(0), schema.parseSimpleIndexes());
50+
return new CharPathOnlySplitter(sep.charAt(0), schema.pathSeparatorEscapeChar(), schema.parseSimpleIndexes());
5151
}
5252
return new StringPathOnlySplitter(sep, schema.parseSimpleIndexes());
5353
}
@@ -139,12 +139,14 @@ public JPropNode splitAndAdd(JPropNode parent,
139139
public static class CharPathOnlySplitter extends JPropPathSplitter
140140
{
141141
protected final char _pathSeparatorChar;
142+
protected final char _pathSeparatorEscapeChar;
142143

143-
public CharPathOnlySplitter(char sepChar, boolean useIndex)
144+
public CharPathOnlySplitter(char sepChar, char pathSeparatorEscapeChar, boolean useIndex)
144145
{
145146
super(useIndex);
146147
_pathSeparatorChar = sepChar;
147-
}
148+
_pathSeparatorEscapeChar = pathSeparatorEscapeChar;
149+
}
148150

149151
@Override
150152
public JPropNode splitAndAdd(JPropNode parent,
@@ -157,16 +159,64 @@ public JPropNode splitAndAdd(JPropNode parent,
157159

158160
while ((ix = key.indexOf(_pathSeparatorChar, start)) >= start) {
159161
if (ix > start) { // segment before separator
162+
if (key.charAt(ix - 1) == _pathSeparatorEscapeChar) { //potentially escaped, so process slowly
163+
return _continueWithEscapes(curr, key, start, value);
164+
}
160165
String segment = key.substring(start, ix);
161166
curr = _addSegment(curr, segment);
162167
}
163168
start = ix + 1;
164-
if (start == key.length()) {
169+
if (start == keyLen) {
165170
break;
166171
}
167172
}
168173
return _lastSegment(curr, key, start, keyLen).setValue(value);
169174
}
175+
176+
// Working character by character to handle escapes is slower
177+
// than using indexOf, so only do it if we have an escape char
178+
// before the path separator char.
179+
// Note that this resets back to the previous start, so one segment
180+
// is scanned twice.
181+
private JPropNode _continueWithEscapes(JPropNode parent, String key, int start, String value) {
182+
JPropNode curr = parent;
183+
184+
int keylen = key.length();
185+
int escCount = 0;
186+
187+
StringBuilder segment = new StringBuilder();
188+
189+
for (int ix = start; ix < keylen; ++ix) {
190+
int cc = key.charAt(ix);
191+
if (cc ==_pathSeparatorEscapeChar) {
192+
escCount++;
193+
} else if (cc == _pathSeparatorChar) {
194+
if (escCount > 0) {
195+
segment.append(key, start, ix - ((escCount + 1) >> 1));
196+
if (escCount % 2 == 0) {
197+
curr = _addSegment(curr, segment.toString());
198+
segment = new StringBuilder();
199+
start = ix + 1;
200+
} else {
201+
segment.append((char) cc);
202+
start = ix + 1;
203+
escCount = 0;
204+
}
205+
} else {
206+
segment.append(key, start, ix);
207+
curr = _addSegment(curr, segment.toString());
208+
segment = new StringBuilder();
209+
start = ix + 1;
210+
}
211+
} else {
212+
escCount = 0;
213+
}
214+
}
215+
segment.append(key, start, keylen);
216+
curr = _addSegment(curr, segment.toString()).setValue(value);
217+
218+
return curr;
219+
}
170220
}
171221

172222
/**

properties/src/test/java/com/fasterxml/jackson/dataformat/javaprop/MapParsingTest.java

+81-5
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,107 @@
77
public class MapParsingTest extends ModuleTestBase
88
{
99
static class MapWrapper {
10-
public Map<String,String> map;
10+
public Map<String,Object> map;
1111
}
1212

13-
private final ObjectMapper MAPPER = newPropertiesMapper();
1413

1514
/*
1615
/**********************************************************************
1716
/* Test methods
1817
/**********************************************************************
1918
*/
2019

21-
public void testMapWithBranch() throws Exception
20+
public void testMapWithBranchNoEscaping() throws Exception
2221
{
22+
ObjectMapper mapper = newPropertiesMapper();
23+
2324
// basically "extra" branch should become as first element, and
2425
// after that ordering by numeric value
2526
final String INPUT = "map=first\n"
2627
+"map.b=second\n"
2728
+"map.xyz=third\n"
29+
+"map.ab\\\\.c=fourth\n"
2830
;
29-
MapWrapper w = MAPPER.readValue(INPUT, MapWrapper.class);
31+
MapWrapper w = mapper.readValue(INPUT, MapWrapper.class);
3032
assertNotNull(w.map);
31-
assertEquals(3, w.map.size());
33+
assertEquals(4, w.map.size());
3234
assertEquals("first", w.map.get(""));
3335
assertEquals("second", w.map.get("b"));
3436
assertEquals("third", w.map.get("xyz"));
37+
assertEquals("fourth", ((Map) w.map.get("ab\\")).get("c"));
38+
}
39+
40+
public void testMapWithBranchBackslashEscape() throws Exception
41+
{
42+
JavaPropsMapper mapper = newPropertiesMapper();
43+
44+
// Lots of backslash escaped values
45+
final String INPUT = "map=first\n"
46+
+"map.b=second\n"
47+
+"map.xyz=third\n"
48+
+"map.ab\\\\.c=fourth\n" // ab\. => ab.c
49+
+"map.ab\\\\cd\\\\.ef\\\\.gh\\\\\\\\ij=fifth\n" // ab\cd\.df\.gh\\ij => ab\cd.df.gh\\ij
50+
+"map.\\\\.=sixth\n" // \. => .
51+
+"map.ab\\\\.d=seventh\n" // ab\.d => ab.d
52+
+"map.ef\\\\\\\\.d=eigth\n" // ef\\.d => ef\->d
53+
+"map.ab\\\\\\\\\\\\.d=ninth\n" // ab\\\.d => ab\.d
54+
+"map.xy\\\\.d.ij=tenth\n" // xy\.d.ij => xy.d->ij
55+
+"map.xy\\\\\\\\.d.ij=eleventh\n" // xy\\.d.ij => xy\->d->ij
56+
+"map.xy\\\\\\\\\\\\.d.ij=twelfth\n" // xy\\\.d => xy\.d->ij
57+
;
58+
MapWrapper w = mapper.reader(new JavaPropsSchema().withPathSeparatorEscapeChar('\\')).readValue(INPUT, MapWrapper.class);
59+
assertNotNull(w.map);
60+
System.out.println(w.map.toString());
61+
assertEquals(12, w.map.size());
62+
assertEquals("first", w.map.get(""));
63+
assertEquals("second", w.map.get("b"));
64+
assertEquals("third", w.map.get("xyz"));
65+
assertEquals("fourth", w.map.get("ab.c"));
66+
assertEquals("fifth", w.map.get("ab\\cd.ef.gh\\\\ij"));
67+
assertEquals("sixth", w.map.get("."));
68+
assertEquals("seventh", w.map.get("ab.d"));
69+
assertEquals("eigth", ((Map) w.map.get("ef\\")).get("d"));
70+
assertEquals("ninth", w.map.get("ab\\.d"));
71+
assertEquals("tenth", ((Map) w.map.get("xy.d")).get("ij"));
72+
assertEquals("eleventh", ((Map) ((Map) w.map.get("xy\\")).get("d")).get("ij"));
73+
assertEquals("twelfth", ((Map) w.map.get("xy\\.d")).get("ij"));
74+
}
75+
76+
77+
public void testMapWithBranchHashEscape() throws Exception
78+
{
79+
JavaPropsMapper mapper = newPropertiesMapper();
80+
81+
// Lots of backslash escaped values
82+
final String INPUT = "map=first\n"
83+
+"map.b=second\n"
84+
+"map.xyz=third\n"
85+
+"map.ab#.c=fourth\n" // ab#. => ab.c
86+
+"map.ab#cd#.ef#.gh##ij=fifth\n" // ab#cd#.df#.gh##ij => ab#cd.df.gh##ij
87+
+"map.#.=sixth\n" // #. => .
88+
+"map.ab#.d=seventh\n" // ab#.d => ab.d
89+
+"map.ef##.d=eigth\n" // ef##.d => ef#->d
90+
+"map.ab###.d=ninth\n" // ab###.d => ab#.d
91+
+"map.xy#.d.ij=tenth\n" // xy#.d.ij => xy.d->ij
92+
+"map.xy##.d.ij=eleventh\n" // xy##.d.ij => xy#->d->ij
93+
+"map.xy###.d.ij=twelfth\n" // xy###.d => xy#.d->ij
94+
;
95+
MapWrapper w = mapper.reader(new JavaPropsSchema().withPathSeparatorEscapeChar('#')).readValue(INPUT, MapWrapper.class);
96+
assertNotNull(w.map);
97+
System.out.println(w.map.toString());
98+
assertEquals(12, w.map.size());
99+
assertEquals("first", w.map.get(""));
100+
assertEquals("second", w.map.get("b"));
101+
assertEquals("third", w.map.get("xyz"));
102+
assertEquals("fourth", w.map.get("ab.c"));
103+
assertEquals("fifth", w.map.get("ab#cd.ef.gh##ij"));
104+
assertEquals("sixth", w.map.get("."));
105+
assertEquals("seventh", w.map.get("ab.d"));
106+
assertEquals("eigth", ((Map) w.map.get("ef#")).get("d"));
107+
assertEquals("ninth", w.map.get("ab#.d"));
108+
assertEquals("tenth", ((Map) w.map.get("xy.d")).get("ij"));
109+
assertEquals("eleventh", ((Map) ((Map) w.map.get("xy#")).get("d")).get("ij"));
110+
assertEquals("twelfth", ((Map) w.map.get("xy#.d")).get("ij"));
35111
}
36112

37113
}

0 commit comments

Comments
 (0)