Skip to content

Simplest fix for issue #169 (escaping separators in JavaPropsMapper). #332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions properties/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,33 @@ Currently existing configuration settings to use can be divide into three groups
* `JavaPropsSchema.withPathSeparator(String)` to assign path separator (except if "" given, same as disabling)
* `JavaPropsSchema.withoutPathSeparator()` to disable use of path logic; if so, only main-level properties are available, with exact property key as name

#### JavaPropsSchema.pathSeparatorEscapeChar

* Marker used to enable the JavaPropsSchema.pathSeparator to be included in key names.
* Default value: '\0' (effectively disabling it).
* Mutator methods
* `JavaPropsSchema.withPathSeparatorEscapeChar(char)` to assign path separator escape char
* Notes
* The escape character is only used if the path separator is a single character.
* The escape character is only used for escaping either the pathSeparator character
or a sequence of escape characters immediately prior to the pathSeparator.
* Any escape character may be used.
* Backslash ('\\') is the most obvious character to use, but be aware that the JDK Properties
loader has its own rules for escape processing (documented in the Javadoc for [Properties.load]
(https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/Properties.html#load(java.io.Reader) )
) that will remove ALL duplicated backslash characters (and also carry out other escape handling)
before the JavaPropsMapper gets to see them.
* Examples
* Given a pathSeparator of "." and an escape char of '#' then
* a#.b
produces a segment called "a.b"
* a##.b
produces a segment called "a#" with a child called "b"
* a###.b
produces a segment called "a#.b"
* a#b
produces a segment called "a#b" - the escape processing is only used immediately prior to the path separator.

### JavaPropsSchema: array representation

#### JavaPropsSchema.firstArrayOffset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ public class JavaPropsSchema
*/
protected String _pathSeparator = ".";

/**
* Default escape character to use for single character path separators
* , enabling the pathSeparator to be included in a segment.
* Note that this is only used if the path separator is a single character.
*
* The default value is NULL ('\0') which effectively disables escape processing.
*
* The escape character is only used for escaping either the pathSeparator character
* or a sequence of escape characters immediately prior to the pathSeparator.
* i.e., if the pathSeparator is "." and the escape char is '#' then "a#.b"
* produces a segment called "a.b", but "a##.b" produces a segment called "a#"
* with a child called "b" and "a###.b" produces a segment called "a#.b".
* Finally, "a#b" produces a segment called "a#b" - the escape processing is only used
* immediately prior to the path separator.
*
* Any escape character may be used.
* Backslash ('\\') is the most obvious candidate but be aware that the JDK Properties
* loader has its own rules for escape processing (documented in the Javadoc for
* <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>
* ) that will remove ALL duplicated backslash characters (and also carry out
* other escape handling) before the JavaPropsMapper gets to see them.
*
* @since 2.14
*/
protected char _pathSeparatorEscapeChar = '\0';

/**
* Default start marker for index access, if any; empty String may be used
* to indicate no marker-based index detection should be made.
Expand Down Expand Up @@ -158,6 +184,7 @@ public JavaPropsSchema() { }
public JavaPropsSchema(JavaPropsSchema base) {
_firstArrayOffset = base._firstArrayOffset;
_pathSeparator = base._pathSeparator;
_pathSeparatorEscapeChar = base._pathSeparatorEscapeChar;
_indexMarker = base._indexMarker;
_parseSimpleIndexes = base._parseSimpleIndexes;
_writeIndexUsingMarkers = base._writeIndexUsingMarkers;
Expand Down Expand Up @@ -211,6 +238,40 @@ public JavaPropsSchema withPathSeparator(String v) {
return s;
}

/**
* Mutant factory method for constructing a new instance with
* a different escape character to use for single character path separators
* , enabling the pathSeparator to be included in a segment.
* Note that this is only used if the path separator is a single character.
*
* The default value is NULL ('\0') which effectively disables escape processing.
*
* The escape character is only used for escaping either the pathSeparator character
* or a sequence of escape characters immediately prior to the pathSeparator.
* i.e., if the pathSeparator is "." and the escape char is '#' then "a#.b"
* produces a segment called "a.b", but "a##.b" produces a segment called "a#"
* with a child called "b" and "a###.b" produces a segment called "a#.b".
* Finally, "a#b" produces a segment called "a#b" - the escape processing is only used
* immediately prior to the path separator.
*
* Any escape character may be used.
* Backslash ('\\') is the most obvious candidate but be aware that the JDK Properties
* loader has its own rules for escape processing (documented in the Javadoc for
* <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>
* ) that will remove ALL duplicated backslash characters (and also carry out
* other escape handling) before the JavaPropsMapper gets to see them.
*
* @since 2.14
*/
public JavaPropsSchema withPathSeparatorEscapeChar(char v) {
if (_equals(v, _pathSeparator)) {
return this;
}
JavaPropsSchema s = new JavaPropsSchema(this);
s._pathSeparatorEscapeChar = v;
return s;
}

/**
* Mutant factory method for constructing a new instance that
* specifies that no "path splitting" is to be done: this is
Expand Down Expand Up @@ -396,6 +457,13 @@ public String pathSeparator() {
return _pathSeparator;
}

/**
* @since 2.14
*/
public char pathSeparatorEscapeChar() {
return _pathSeparatorEscapeChar;
}

/**
* @since 2.10
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private static JPropPathSplitter pathOnlySplitter(JavaPropsSchema schema)
}
// otherwise it's still quite simple
if (sep.length() == 1) {
return new CharPathOnlySplitter(sep.charAt(0), schema.parseSimpleIndexes());
return new CharPathOnlySplitter(sep.charAt(0), schema.pathSeparatorEscapeChar(), schema.parseSimpleIndexes());
}
return new StringPathOnlySplitter(sep, schema.parseSimpleIndexes());
}
Expand Down Expand Up @@ -139,12 +139,14 @@ public JPropNode splitAndAdd(JPropNode parent,
public static class CharPathOnlySplitter extends JPropPathSplitter
{
protected final char _pathSeparatorChar;
protected final char _pathSeparatorEscapeChar;

public CharPathOnlySplitter(char sepChar, boolean useIndex)
public CharPathOnlySplitter(char sepChar, char pathSeparatorEscapeChar, boolean useIndex)
{
super(useIndex);
_pathSeparatorChar = sepChar;
}
_pathSeparatorEscapeChar = pathSeparatorEscapeChar;
}

@Override
public JPropNode splitAndAdd(JPropNode parent,
Expand All @@ -157,16 +159,64 @@ public JPropNode splitAndAdd(JPropNode parent,

while ((ix = key.indexOf(_pathSeparatorChar, start)) >= start) {
if (ix > start) { // segment before separator
if (key.charAt(ix - 1) == _pathSeparatorEscapeChar) { //potentially escaped, so process slowly
return _continueWithEscapes(curr, key, start, value);
}
String segment = key.substring(start, ix);
curr = _addSegment(curr, segment);
}
start = ix + 1;
if (start == key.length()) {
if (start == keyLen) {
break;
}
}
return _lastSegment(curr, key, start, keyLen).setValue(value);
}

// Working character by character to handle escapes is slower
// than using indexOf, so only do it if we have an escape char
// before the path separator char.
// Note that this resets back to the previous start, so one segment
// is scanned twice.
private JPropNode _continueWithEscapes(JPropNode parent, String key, int start, String value) {
JPropNode curr = parent;

int keylen = key.length();
int escCount = 0;

StringBuilder segment = new StringBuilder();

for (int ix = start; ix < keylen; ++ix) {
int cc = key.charAt(ix);
if (cc ==_pathSeparatorEscapeChar) {
escCount++;
} else if (cc == _pathSeparatorChar) {
if (escCount > 0) {
segment.append(key, start, ix - ((escCount + 1) >> 1));
if (escCount % 2 == 0) {
curr = _addSegment(curr, segment.toString());
segment = new StringBuilder();
start = ix + 1;
} else {
segment.append((char) cc);
start = ix + 1;
escCount = 0;
}
} else {
segment.append(key, start, ix);
curr = _addSegment(curr, segment.toString());
segment = new StringBuilder();
start = ix + 1;
}
} else {
escCount = 0;
}
}
segment.append(key, start, keylen);
curr = _addSegment(curr, segment.toString()).setValue(value);

return curr;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,107 @@
public class MapParsingTest extends ModuleTestBase
{
static class MapWrapper {
public Map<String,String> map;
public Map<String,Object> map;
}

private final ObjectMapper MAPPER = newPropertiesMapper();

/*
/**********************************************************************
/* Test methods
/**********************************************************************
*/

public void testMapWithBranch() throws Exception
public void testMapWithBranchNoEscaping() throws Exception
{
ObjectMapper mapper = newPropertiesMapper();

// basically "extra" branch should become as first element, and
// after that ordering by numeric value
final String INPUT = "map=first\n"
+"map.b=second\n"
+"map.xyz=third\n"
+"map.ab\\\\.c=fourth\n"
;
MapWrapper w = MAPPER.readValue(INPUT, MapWrapper.class);
MapWrapper w = mapper.readValue(INPUT, MapWrapper.class);
assertNotNull(w.map);
assertEquals(3, w.map.size());
assertEquals(4, w.map.size());
assertEquals("first", w.map.get(""));
assertEquals("second", w.map.get("b"));
assertEquals("third", w.map.get("xyz"));
assertEquals("fourth", ((Map) w.map.get("ab\\")).get("c"));
}

public void testMapWithBranchBackslashEscape() throws Exception
{
JavaPropsMapper mapper = newPropertiesMapper();

// Lots of backslash escaped values
final String INPUT = "map=first\n"
+"map.b=second\n"
+"map.xyz=third\n"
+"map.ab\\\\.c=fourth\n" // ab\. => ab.c
+"map.ab\\\\cd\\\\.ef\\\\.gh\\\\\\\\ij=fifth\n" // ab\cd\.df\.gh\\ij => ab\cd.df.gh\\ij
+"map.\\\\.=sixth\n" // \. => .
+"map.ab\\\\.d=seventh\n" // ab\.d => ab.d
+"map.ef\\\\\\\\.d=eigth\n" // ef\\.d => ef\->d
+"map.ab\\\\\\\\\\\\.d=ninth\n" // ab\\\.d => ab\.d
+"map.xy\\\\.d.ij=tenth\n" // xy\.d.ij => xy.d->ij
+"map.xy\\\\\\\\.d.ij=eleventh\n" // xy\\.d.ij => xy\->d->ij
+"map.xy\\\\\\\\\\\\.d.ij=twelfth\n" // xy\\\.d => xy\.d->ij
;
MapWrapper w = mapper.reader(new JavaPropsSchema().withPathSeparatorEscapeChar('\\')).readValue(INPUT, MapWrapper.class);
assertNotNull(w.map);
System.out.println(w.map.toString());
assertEquals(12, w.map.size());
assertEquals("first", w.map.get(""));
assertEquals("second", w.map.get("b"));
assertEquals("third", w.map.get("xyz"));
assertEquals("fourth", w.map.get("ab.c"));
assertEquals("fifth", w.map.get("ab\\cd.ef.gh\\\\ij"));
assertEquals("sixth", w.map.get("."));
assertEquals("seventh", w.map.get("ab.d"));
assertEquals("eigth", ((Map) w.map.get("ef\\")).get("d"));
assertEquals("ninth", w.map.get("ab\\.d"));
assertEquals("tenth", ((Map) w.map.get("xy.d")).get("ij"));
assertEquals("eleventh", ((Map) ((Map) w.map.get("xy\\")).get("d")).get("ij"));
assertEquals("twelfth", ((Map) w.map.get("xy\\.d")).get("ij"));
}


public void testMapWithBranchHashEscape() throws Exception
{
JavaPropsMapper mapper = newPropertiesMapper();

// Lots of backslash escaped values
final String INPUT = "map=first\n"
+"map.b=second\n"
+"map.xyz=third\n"
+"map.ab#.c=fourth\n" // ab#. => ab.c
+"map.ab#cd#.ef#.gh##ij=fifth\n" // ab#cd#.df#.gh##ij => ab#cd.df.gh##ij
+"map.#.=sixth\n" // #. => .
+"map.ab#.d=seventh\n" // ab#.d => ab.d
+"map.ef##.d=eigth\n" // ef##.d => ef#->d
+"map.ab###.d=ninth\n" // ab###.d => ab#.d
+"map.xy#.d.ij=tenth\n" // xy#.d.ij => xy.d->ij
+"map.xy##.d.ij=eleventh\n" // xy##.d.ij => xy#->d->ij
+"map.xy###.d.ij=twelfth\n" // xy###.d => xy#.d->ij
;
MapWrapper w = mapper.reader(new JavaPropsSchema().withPathSeparatorEscapeChar('#')).readValue(INPUT, MapWrapper.class);
assertNotNull(w.map);
System.out.println(w.map.toString());
assertEquals(12, w.map.size());
assertEquals("first", w.map.get(""));
assertEquals("second", w.map.get("b"));
assertEquals("third", w.map.get("xyz"));
assertEquals("fourth", w.map.get("ab.c"));
assertEquals("fifth", w.map.get("ab#cd.ef.gh##ij"));
assertEquals("sixth", w.map.get("."));
assertEquals("seventh", w.map.get("ab.d"));
assertEquals("eigth", ((Map) w.map.get("ef#")).get("d"));
assertEquals("ninth", w.map.get("ab#.d"));
assertEquals("tenth", ((Map) w.map.get("xy.d")).get("ij"));
assertEquals("eleventh", ((Map) ((Map) w.map.get("xy#")).get("d")).get("ij"));
assertEquals("twelfth", ((Map) w.map.get("xy#.d")).get("ij"));
}

}