Skip to content

Commit f008130

Browse files
committed
Fix #306
1 parent 7e57c55 commit f008130

File tree

8 files changed

+424
-6
lines changed

8 files changed

+424
-6
lines changed

release-notes/VERSION

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ JSON library.
1919
#17: Add 'JsonGenerator.writeString(Reader r, int charLength)'
2020
(constributed by Logan W)
2121
#304: Optimize `NumberOutput.outputLong()` method
22+
#306: Add new method in `JsonStreamContext` to construct `JsonPointer`
2223
#312: Add `JsonProcessingException.clearLocation()` to allow clearing
2324
possibly security-sensitive information
2425
(contributed by Alex Y)

src/main/java/com/fasterxml/jackson/core/JsonPointer.java

+100-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* It may be used in future for filtering of streaming JSON content
1212
* as well (not implemented yet for 2.3).
1313
*<p>
14-
* Instances are fully immutable and can be shared, cached.
14+
* Instances are fully immutable and can be cached, shared between threads.
1515
*
1616
* @author Tatu Saloranta
1717
*
@@ -137,6 +137,86 @@ public static JsonPointer compile(String input) throws IllegalArgumentException
137137
*/
138138
public static JsonPointer valueOf(String input) { return compile(input); }
139139

140+
/**
141+
* Factory method that will construct a pointer instance that describes
142+
* path to location given {@link JsonStreamContext} points to.
143+
*
144+
* @param context Context to build pointer expression fot
145+
* @param includeRoot Whether to include number offset for virtual "root context"
146+
* or not.
147+
*
148+
* @since 2.9
149+
*/
150+
public static JsonPointer forPath(JsonStreamContext context,
151+
boolean includeRoot)
152+
{
153+
// First things first: last segment may be for START_ARRAY/START_OBJECT,
154+
// in which case it does not yet point to anything, and should be skipped
155+
if (context == null) {
156+
return EMPTY;
157+
}
158+
if (!context.hasPathSegment()) {
159+
// one special case; do not prune root if we need it
160+
if (!(includeRoot && context.inRoot() && context.hasCurrentIndex())) {
161+
context = context.getParent();
162+
}
163+
}
164+
JsonPointer tail = null;
165+
166+
for (; context != null; context = context.getParent()) {
167+
if (context.inObject()) {
168+
String seg = context.getCurrentName();
169+
if (seg == null) { // is this legal?
170+
seg = "";
171+
}
172+
tail = new JsonPointer(_fullPath(tail, seg), seg, tail);
173+
} else if (context.inArray() || includeRoot) {
174+
int ix = context.getCurrentIndex();
175+
String ixStr = String.valueOf(ix);
176+
tail = new JsonPointer(_fullPath(tail, ixStr), ixStr, ix, tail);
177+
}
178+
// NOTE: this effectively drops ROOT node(s); should have 1 such node,
179+
// as the last one, but we don't have to care (probably some paths have
180+
// no root, for example)
181+
}
182+
if (tail == null) {
183+
return EMPTY;
184+
}
185+
return tail;
186+
}
187+
188+
private static String _fullPath(JsonPointer tail, String segment)
189+
{
190+
if (tail == null) {
191+
StringBuilder sb = new StringBuilder(segment.length()+1);
192+
sb.append('/');
193+
_appendEscaped(sb, segment);
194+
return sb.toString();
195+
}
196+
String tailDesc = tail._asString;
197+
StringBuilder sb = new StringBuilder(segment.length() + 1 + tailDesc.length());
198+
sb.append('/');
199+
_appendEscaped(sb, segment);
200+
sb.append(tailDesc);
201+
return sb.toString();
202+
}
203+
204+
private static void _appendEscaped(StringBuilder sb, String segment)
205+
{
206+
for (int i = 0, end = segment.length(); i < end; ++i) {
207+
char c = segment.charAt(i);
208+
if (c == '/') {
209+
sb.append("~1");
210+
continue;
211+
}
212+
if (c == '~') {
213+
sb.append("~0");
214+
continue;
215+
}
216+
sb.append(c);
217+
}
218+
}
219+
140220
/* Factory method that composes a pointer instance, given a set
141221
* of 'raw' segments: raw meaning that no processing will be done,
142222
* no escaping may is present.
@@ -189,13 +269,32 @@ public JsonPointer last() {
189269
return current;
190270
}
191271

272+
/**
273+
* Mutant factory method that will return
274+
*<ul>
275+
* <li>`tail` if `this` instance is "empty" pointer, OR
276+
* </li>
277+
* <li>`this` instance if `tail` is "empty" pointer, OR
278+
* </li>
279+
* <li>Newly constructed {@link JsonPointer} instance that starts with all segments
280+
* of `this`, followed by all segments of `tail`.
281+
* </li>
282+
*</ul>
283+
*
284+
* @param tail {@link JsonPointer} instance to append to this one, to create a new pointer instance
285+
*
286+
* @return Either `this` instance, `tail`, or a newly created combination, as per description above.
287+
*/
192288
public JsonPointer append(JsonPointer tail) {
193289
if (this == EMPTY) {
194290
return tail;
195291
}
196292
if (tail == EMPTY) {
197293
return this;
198294
}
295+
// 21-Mar-2017, tatu: Not superbly efficient; could probably improve by not concatenating,
296+
// re-decoding -- by stitching together segments -- but for now should be fine.
297+
199298
String currentJsonPointer = _asString;
200299
if (currentJsonPointer.endsWith("/")) {
201300
//removes final slash

src/main/java/com/fasterxml/jackson/core/JsonStreamContext.java

+66-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public String typeDesc() {
101101
}
102102
return "?";
103103
}
104-
104+
105105
/**
106106
* @return Number of entries that are complete and started.
107107
*/
@@ -112,13 +112,53 @@ public String typeDesc() {
112112
*/
113113
public final int getCurrentIndex() { return (_index < 0) ? 0 : _index; }
114114

115+
/**
116+
* Method that may be called to verify whether this context has valid index:
117+
* will return `false` before the first entry of Object context or before
118+
* first element of Array context; otherwise returns `true`.
119+
*
120+
* @since 2.9
121+
*/
122+
public boolean hasCurrentIndex() { return _index >= 0; }
123+
124+
/**
125+
* Method that may be called to check if this context is either:
126+
*<ul>
127+
* <li>Object, with at least one entry written (partially or completely)
128+
* </li>
129+
* <li>Array, with at least one entry written (partially or completely)
130+
* </li>
131+
*</ul>
132+
* and if so, return `true`; otherwise return `false`. Latter case includes
133+
* Root context (always), and Object/Array contexts before any entries/elements
134+
* have been read or written.
135+
*<p>
136+
* Method is mostly used to determine whether this context should be used for
137+
* constructing {@link JsonPointer}
138+
*
139+
* @since 2.9
140+
*/
141+
public boolean hasPathSegment() {
142+
if (_type == TYPE_OBJECT) {
143+
return hasCurrentName();
144+
} else if (_type == TYPE_ARRAY) {
145+
return hasCurrentIndex();
146+
}
147+
return false;
148+
}
149+
115150
/**
116151
* Method for accessing name associated with the current location.
117152
* Non-null for <code>FIELD_NAME</code> and value events that directly
118153
* follow field names; null for root level and array values.
119154
*/
120155
public abstract String getCurrentName();
121156

157+
/**
158+
* @since 2.9
159+
*/
160+
public boolean hasCurrentName() { return getCurrentName() != null; }
161+
122162
/**
123163
* Method for accessing currently active value being used by data-binding
124164
* (as the source of streaming data to write, or destination of data being
@@ -145,4 +185,29 @@ public Object getCurrentValue() {
145185
* @since 2.5
146186
*/
147187
public void setCurrentValue(Object v) { }
188+
189+
/**
190+
* Factory method for constructing a {@link JsonPointer} that points to the current
191+
* location within the stream that this context is for, excluding information about
192+
* "root context" (only relevant for multi-root-value cases)
193+
*
194+
* @since 2.9
195+
*/
196+
public JsonPointer pathAsPointer() {
197+
return JsonPointer.forPath(this, false);
198+
}
199+
200+
/**
201+
* Factory method for constructing a {@link JsonPointer} that points to the current
202+
* location within the stream that this context is for, optionally including
203+
* "root value index"
204+
*
205+
* @param includeRoot Whether root-value offset is included as the first segment or not;
206+
*
207+
*
208+
* @since 2.9
209+
*/
210+
public JsonPointer pathAsPointer(boolean includeRoot) {
211+
return JsonPointer.forPath(this, includeRoot);
212+
}
148213
}

src/main/java/com/fasterxml/jackson/core/filter/TokenFilterContext.java

+2
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ public void setCurrentValue(Object v) { }
275275

276276
@Override public final TokenFilterContext getParent() { return _parent; }
277277
@Override public final String getCurrentName() { return _currentName; }
278+
// @since 2.9
279+
@Override public boolean hasCurrentName() { return _currentName != null; }
278280

279281
public TokenFilter getFilter() { return _filter; }
280282
public boolean isStartHandled() { return _startHandled; }

src/main/java/com/fasterxml/jackson/core/json/JsonReadContext.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ protected void reset(int type, int lineNr, int colNr) {
7777
}
7878

7979
/*
80-
public void trackDups(JsonParser jp) {
81-
_dups = DupDetector.rootDetector(jp);
80+
public void trackDups(JsonParser p) {
81+
_dups = DupDetector.rootDetector(p);
8282
}
8383
*/
8484

@@ -140,6 +140,10 @@ public JsonReadContext createChildObjectContext(int lineNr, int colNr) {
140140
*/
141141

142142
@Override public String getCurrentName() { return _currentName; }
143+
144+
// @since 2.9
145+
@Override public boolean hasCurrentName() { return _currentName != null; }
146+
143147
@Override public JsonReadContext getParent() { return _parent; }
144148

145149
/**

src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java

+2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public JsonWriteContext createChildObjectContext() {
135135

136136
@Override public final JsonWriteContext getParent() { return _parent; }
137137
@Override public final String getCurrentName() { return _currentName; }
138+
// @since 2.9
139+
@Override public boolean hasCurrentName() { return _currentName != null; }
138140

139141
/**
140142
* Method that can be used to both clear the accumulated references

0 commit comments

Comments
 (0)