JPropPathSplitter.java
package com.fasterxml.jackson.dataformat.javaprop.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.fasterxml.jackson.dataformat.javaprop.JavaPropsSchema;
/**
* Helper class used for splitting a flattened property key into
* nested/structured path that can be used to traverse and/or define
* hierarchic structure.
*/
public abstract class JPropPathSplitter
{
protected final boolean _useSimpleIndex;
protected JPropPathSplitter(boolean useSimpleIndex) {
_useSimpleIndex = useSimpleIndex;
}
public static JPropPathSplitter create(JavaPropsSchema schema)
{
// will never be null
String sep = schema.pathSeparator();
final Markers indexMarker = schema.indexMarker();
// First: index marker in use?
if (indexMarker == null) { // nope, only path separator, if anything
// and if no separator, can use bogus "splitter":
return pathOnlySplitter(schema);
}
// Yes, got index marker to use. But separator?
if (sep.isEmpty()) {
return new IndexOnlySplitter(schema.parseSimpleIndexes(), indexMarker);
}
return new FullSplitter(sep, schema.parseSimpleIndexes(),
indexMarker,
pathOnlySplitter(schema),
schema.prefix());
}
private static JPropPathSplitter pathOnlySplitter(JavaPropsSchema schema)
{
String sep = schema.pathSeparator();
if (sep.isEmpty()) {
return NonSplitting.instance;
}
// otherwise it's still quite simple
if (sep.length() == 1) {
return new CharPathOnlySplitter(sep.charAt(0), schema.pathSeparatorEscapeChar(), schema.parseSimpleIndexes());
}
return new StringPathOnlySplitter(sep, schema.parseSimpleIndexes());
}
/**
* Main access method for splitting key into one or more segments and using
* segmentation to add the String value as a node in its proper location.
*
* @return Newly added node
*/
public abstract JPropNode splitAndAdd(JPropNode parent,
String key, String value);
/*
/**********************************************************************
/* Helper methods for implementations
/**********************************************************************
*/
protected JPropNode _addSegment(JPropNode parent, String segment)
{
if (_useSimpleIndex) {
int ix = _asInt(segment);
if (ix >= 0) {
return parent.addByIndex(ix);
}
}
return parent.addByName(segment);
}
protected JPropNode _lastSegment(JPropNode parent, String path, int start, int end)
{
if (start < end) {
if (start > 0) {
path = path.substring(start);
}
parent = _addSegment(parent, path);
}
return parent;
}
protected int _asInt(String segment) {
final int len = segment.length();
// do not allow ridiculously long numbers as indexes
if ((len == 0) || (len > 9)) {
return -1;
}
char c = segment.charAt(0);
if ((c > '9') || (c < '0')) {
return -1;
}
for (int i = 0; i < len; ++i) {
c = segment.charAt(i);
if ((c > '9') || (c < '0')) {
return -1;
}
}
return Integer.parseInt(segment);
}
/*
/**********************************************************************
/* Implementations
/**********************************************************************
*/
/**
* "No-op" implementation that does no splitting and simply adds entries
* as is.
*/
public static class NonSplitting extends JPropPathSplitter
{
public final static NonSplitting instance = new NonSplitting();
private NonSplitting() { super(false); }
@Override
public JPropNode splitAndAdd(JPropNode parent,
String key, String value)
{
return parent.addByName(key).setValue(value);
}
}
/**
* Simple variant where we only have path separator, and optional "segment
* is index iff value is integer number"
*/
public static class CharPathOnlySplitter extends JPropPathSplitter
{
protected final char _pathSeparatorChar;
protected final char _pathSeparatorEscapeChar;
public CharPathOnlySplitter(char sepChar, char pathSeparatorEscapeChar, boolean useIndex)
{
super(useIndex);
_pathSeparatorChar = sepChar;
_pathSeparatorEscapeChar = pathSeparatorEscapeChar;
}
@Override
public JPropNode splitAndAdd(JPropNode parent,
String key, String value)
{
JPropNode curr = parent;
int start = 0;
final int keyLen = key.length();
int ix;
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 == 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;
}
}
/**
* Simple variant where we only have path separator, and optional "segment
* is index iff value is integer number"
*/
public static class StringPathOnlySplitter extends JPropPathSplitter
{
protected final String _pathSeparator;
protected final int _pathSeparatorLength;
public StringPathOnlySplitter(String pathSeparator, boolean useIndex)
{
super(useIndex);
_pathSeparator = pathSeparator;
_pathSeparatorLength = pathSeparator.length();
}
@Override
public JPropNode splitAndAdd(JPropNode parent,
String key, String value)
{
JPropNode curr = parent;
int start = 0;
final int keyLen = key.length();
int ix;
while ((ix = key.indexOf(_pathSeparator, start)) >= start) {
if (ix > start) { // segment before separator
String segment = key.substring(start, ix);
curr = _addSegment(curr, segment);
}
start = ix + _pathSeparatorLength;
if (start == key.length()) {
break;
}
}
return _lastSegment(curr, key, start, keyLen).setValue(value);
}
}
/**
* Special variant that does not use path separator, but does allow
* index indicator, at the end of path.
*/
public static class IndexOnlySplitter extends JPropPathSplitter
{
protected final Pattern _indexMatch;
public IndexOnlySplitter(boolean useSimpleIndex,
Markers indexMarker)
{
super(useSimpleIndex);
_indexMatch = Pattern.compile(String.format("(.*)%s(\\d{1,9})%s$",
Pattern.quote(indexMarker.getStart()),
Pattern.quote(indexMarker.getEnd())));
}
@Override
public JPropNode splitAndAdd(JPropNode parent,
String key, String value)
{
Matcher m = _indexMatch.matcher(key);
// short-cut for common case of no index:
if (!m.matches()) {
return _addSegment(parent, key).setValue(value);
}
// otherwise we need recursion as we "peel" away layers
return _splitMore(parent, m.group(1), m.group(2))
.setValue(value);
}
protected JPropNode _splitMore(JPropNode parent, String prefix, String indexStr)
{
int ix = Integer.parseInt(indexStr);
Matcher m = _indexMatch.matcher(prefix);
if (!m.matches()) {
parent = _addSegment(parent, prefix);
} else {
parent = _splitMore(parent, m.group(1), m.group(2));
}
return parent.addByIndex(ix);
}
}
/**
* Instance that supports both path separator and index markers
* (and possibly also "simple" indexes)
*/
public static class FullSplitter extends JPropPathSplitter
{
protected final Pattern _indexMatch;
// small but important optimization for cases where index markers are absent
protected final int _indexFirstChar;
protected final JPropPathSplitter _simpleSplitter;
/**
* @since 2.10
*/
protected final String _prefix;
public FullSplitter(String pathSeparator, boolean useSimpleIndex,
Markers indexMarker, JPropPathSplitter fallbackSplitter,
String prefix)
{
super(useSimpleIndex);
String startMarker = indexMarker.getStart();
_indexFirstChar = startMarker.charAt(0);
_simpleSplitter = fallbackSplitter;
if (prefix == null || prefix.isEmpty()) {
_prefix = null;
} else {
_prefix = prefix + pathSeparator;
}
_indexMatch = Pattern.compile(String.format
("(%s)|(%s(\\d{1,9})%s)",
Pattern.quote(pathSeparator),
Pattern.quote(startMarker),
Pattern.quote(indexMarker.getEnd())));
}
@Override
public JPropNode splitAndAdd(JPropNode parent,
String key, String value)
{
// [dataformats-text#100]: handle possible prefix
if (_prefix != null) {
if (!key.startsWith(_prefix)) {
return null;
}
key = key.substring(_prefix.length());
}
if (key.indexOf(_indexFirstChar) < 0) { // no index start marker
return _simpleSplitter.splitAndAdd(parent, key, value);
}
Matcher m = _indexMatch.matcher(key);
int start = 0;
while (m.find()) {
// which match did we get? Either path separator (1), or index (2)
int ix = m.start(1);
if (ix >= 0) { // path separator...
if (ix > start) {
String segment = key.substring(start, ix);
parent = _addSegment(parent, segment);
}
start = m.end(1);
continue;
}
// no, index marker, with contents
ix = m.start(2);
if (ix > start) {
String segment = key.substring(start, ix);
parent = _addSegment(parent, segment);
}
start = m.end(2);
ix = Integer.parseInt(m.group(3));
parent = parent.addByIndex(ix);
}
return _lastSegment(parent, key, start, key.length()).setValue(value);
}
}
}