Skip to content

Commit 5f7aa1a

Browse files
committed
Merge branch '1.2.x'
2 parents 645a60a + a084175 commit 5f7aa1a

File tree

10 files changed

+427
-197
lines changed

10 files changed

+427
-197
lines changed

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2015 the original author or authors.
2+
* Copyright 2014-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,7 +31,8 @@ public class FieldDoesNotExistException extends RuntimeException {
3131
*
3232
* @param fieldPath the path of the field that does not exist
3333
*/
34-
public FieldDoesNotExistException(JsonFieldPath fieldPath) {
34+
public FieldDoesNotExistException(String fieldPath) {
3535
super("The payload does not contain a field with the path '" + fieldPath + "'");
3636
}
37+
3738
}

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.fasterxml.jackson.databind.ObjectMapper;
2323

2424
import org.springframework.http.MediaType;
25+
import org.springframework.restdocs.payload.JsonFieldPath.PathType;
26+
import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField;
2527

2628
/**
2729
* A {@link PayloadSubsectionExtractor} that extracts the subsection of the JSON payload
@@ -66,20 +68,20 @@ protected FieldPathPayloadSubsectionExtractor(String fieldPath, String subsectio
6668
public byte[] extractSubsection(byte[] payload, MediaType contentType) {
6769
ObjectMapper objectMapper = new ObjectMapper();
6870
try {
69-
JsonFieldPath compiledPath = JsonFieldPath.compile(this.fieldPath);
70-
Object extracted = new JsonFieldProcessor().extract(compiledPath,
71-
objectMapper.readValue(payload, Object.class));
72-
if (extracted instanceof List && !compiledPath.isPrecise()) {
73-
List<?> extractedList = (List<?>) extracted;
71+
ExtractedField extractedField = new JsonFieldProcessor().extract(
72+
this.fieldPath, objectMapper.readValue(payload, Object.class));
73+
Object value = extractedField.getValue();
74+
if (value instanceof List && extractedField.getType() == PathType.MULTI) {
75+
List<?> extractedList = (List<?>) value;
7476
if (extractedList.size() == 1) {
75-
extracted = extractedList.get(0);
77+
value = extractedList.get(0);
7678
}
7779
else {
7880
throw new PayloadHandlingException(this.fieldPath
7981
+ " does not uniquely identify a subsection of the payload");
8082
}
8183
}
82-
return objectMapper.writeValueAsBytes(extracted);
84+
return objectMapper.writeValueAsBytes(value);
8385
}
8486
catch (IOException ex) {
8587
throw new PayloadHandlingException(ex);

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ public List<FieldDescriptor> findMissingFields(
5252
List<FieldDescriptor> missingFields = new ArrayList<>();
5353
Object payload = readContent();
5454
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
55-
if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField(
56-
JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) {
55+
if (!fieldDescriptor.isOptional() && !this.fieldProcessor
56+
.hasField(fieldDescriptor.getPath(), payload)) {
5757
missingFields.add(fieldDescriptor);
5858
}
5959
}
@@ -65,12 +65,11 @@ public List<FieldDescriptor> findMissingFields(
6565
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
6666
Object content = readContent();
6767
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
68-
JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath());
6968
if (describesSubsection(fieldDescriptor)) {
70-
this.fieldProcessor.removeSubsection(path, content);
69+
this.fieldProcessor.removeSubsection(fieldDescriptor.getPath(), content);
7170
}
7271
else {
73-
this.fieldProcessor.remove(path, content);
72+
this.fieldProcessor.remove(fieldDescriptor.getPath(), content);
7473
}
7574
}
7675
if (!isEmpty(content)) {
@@ -107,7 +106,7 @@ private boolean isEmpty(Object object) {
107106
@Override
108107
public Object determineFieldType(FieldDescriptor fieldDescriptor) {
109108
if (fieldDescriptor.getType() == null) {
110-
return this.fieldTypeResolver.resolveFieldType(fieldDescriptor.getPath(),
109+
return this.fieldTypeResolver.resolveFieldType(fieldDescriptor,
111110
readContent());
112111
}
113112
if (!(fieldDescriptor.getType() instanceof JsonFieldType)) {
@@ -116,7 +115,7 @@ public Object determineFieldType(FieldDescriptor fieldDescriptor) {
116115
JsonFieldType descriptorFieldType = (JsonFieldType) fieldDescriptor.getType();
117116
try {
118117
JsonFieldType actualFieldType = this.fieldTypeResolver
119-
.resolveFieldType(fieldDescriptor.getPath(), readContent());
118+
.resolveFieldType(fieldDescriptor, readContent());
120119
if (descriptorFieldType == JsonFieldType.VARIES
121120
|| descriptorFieldType == actualFieldType
122121
|| (fieldDescriptor.isOptional()

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2015 the original author or authors.
2+
* Copyright 2014-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@
2727
*
2828
* @author Andy Wilkinson
2929
* @author Jeremy Rickard
30-
*
3130
*/
3231
final class JsonFieldPath {
3332

@@ -41,24 +40,16 @@ final class JsonFieldPath {
4140

4241
private final List<String> segments;
4342

44-
private final boolean precise;
45-
46-
private final boolean array;
43+
private final PathType type;
4744

48-
private JsonFieldPath(String rawPath, List<String> segments, boolean precise,
49-
boolean array) {
45+
private JsonFieldPath(String rawPath, List<String> segments, PathType type) {
5046
this.rawPath = rawPath;
5147
this.segments = segments;
52-
this.precise = precise;
53-
this.array = array;
48+
this.type = type;
5449
}
5550

56-
boolean isPrecise() {
57-
return this.precise;
58-
}
59-
60-
boolean isArray() {
61-
return this.array;
51+
PathType getType() {
52+
return this.type;
6253
}
6354

6455
List<String> getSegments() {
@@ -72,9 +63,8 @@ public String toString() {
7263

7364
static JsonFieldPath compile(String path) {
7465
List<String> segments = extractSegments(path);
75-
String leafSegment = segments.get(segments.size() - 1);
76-
return new JsonFieldPath(path, segments, matchesSingleValue(segments),
77-
isArraySegment(leafSegment) || isWildcardSegment(leafSegment));
66+
return new JsonFieldPath(path, segments,
67+
matchesSingleValue(segments) ? PathType.SINGLE : PathType.MULTI);
7868
}
7969

8070
static boolean isArraySegment(String segment) {
@@ -84,8 +74,9 @@ static boolean isArraySegment(String segment) {
8474
static boolean matchesSingleValue(List<String> segments) {
8575
Iterator<String> iterator = segments.iterator();
8676
while (iterator.hasNext()) {
87-
String next = iterator.next();
88-
if ((isArraySegment(next) || isWildcardSegment(next)) && iterator.hasNext()) {
77+
String segment = iterator.next();
78+
if ((isArraySegment(segment) && iterator.hasNext())
79+
|| isWildcardSegment(segment)) {
8980
return false;
9081
}
9182
}
@@ -132,4 +123,19 @@ private static List<String> extractDotSeparatedSegments(String path) {
132123
}
133124
return segments;
134125
}
126+
127+
static enum PathType {
128+
129+
/**
130+
* The path identifies a single item in the payload
131+
*/
132+
SINGLE,
133+
134+
/**
135+
* The path identifies multiple items in the payload
136+
*/
137+
MULTI;
138+
139+
}
140+
135141
}

spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java

Lines changed: 115 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.List;
2323
import java.util.Map;
2424

25+
import org.springframework.restdocs.payload.JsonFieldPath.PathType;
26+
2527
/**
2628
* A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be
2729
* extracted and removed.
@@ -31,15 +33,16 @@
3133
*/
3234
final class JsonFieldProcessor {
3335

34-
boolean hasField(final JsonFieldPath fieldPath, Object payload) {
36+
boolean hasField(String path, Object payload) {
3537
HasFieldMatchCallback callback = new HasFieldMatchCallback();
36-
traverse(new ProcessingContext(payload, fieldPath), callback);
38+
traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)), callback);
3739
return callback.fieldFound();
3840
}
3941

40-
Object extract(JsonFieldPath path, Object payload) {
42+
ExtractedField extract(String path, Object payload) {
43+
JsonFieldPath compiledPath = JsonFieldPath.compile(path);
4144
final List<Object> matches = new ArrayList<>();
42-
traverse(new ProcessingContext(payload, path), new MatchCallback() {
45+
traverse(new ProcessingContext(payload, compiledPath), new MatchCallback() {
4346

4447
@Override
4548
public void foundMatch(Match match) {
@@ -55,44 +58,43 @@ public void absent() {
5558
if (matches.isEmpty()) {
5659
throw new FieldDoesNotExistException(path);
5760
}
58-
if ((!path.isArray()) && path.isPrecise()) {
59-
return matches.get(0);
60-
}
61-
else {
62-
return matches;
63-
}
61+
return new ExtractedField(
62+
compiledPath.getType() == PathType.SINGLE ? matches.get(0) : matches,
63+
compiledPath.getType());
6464
}
6565

66-
void remove(final JsonFieldPath path, Object payload) {
67-
traverse(new ProcessingContext(payload, path), new MatchCallback() {
66+
void remove(String path, Object payload) {
67+
traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)),
68+
new MatchCallback() {
6869

69-
@Override
70-
public void foundMatch(Match match) {
71-
match.remove();
72-
}
70+
@Override
71+
public void foundMatch(Match match) {
72+
match.remove();
73+
}
7374

74-
@Override
75-
public void absent() {
75+
@Override
76+
public void absent() {
7677

77-
}
78+
}
7879

79-
});
80+
});
8081
}
8182

82-
void removeSubsection(final JsonFieldPath path, Object payload) {
83-
traverse(new ProcessingContext(payload, path), new MatchCallback() {
83+
void removeSubsection(String path, Object payload) {
84+
traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)),
85+
new MatchCallback() {
8486

85-
@Override
86-
public void foundMatch(Match match) {
87-
match.removeSubsection();
88-
}
87+
@Override
88+
public void foundMatch(Match match) {
89+
match.removeSubsection();
90+
}
8991

90-
@Override
91-
public void absent() {
92+
@Override
93+
public void absent() {
9294

93-
}
95+
}
9496

95-
});
97+
});
9698
}
9799

98100
private void traverse(ProcessingContext context, MatchCallback matchCallback) {
@@ -115,6 +117,22 @@ private void handleCollectionPayload(ProcessingContext context,
115117

116118
private void handleCollectionPayload(Collection<?> collection,
117119
MatchCallback matchCallback, ProcessingContext context) {
120+
if (context.isLeaf()) {
121+
matchCallback.foundMatch(
122+
new LeafCollectionMatch(collection, context.getParentMatch()));
123+
}
124+
else {
125+
Iterator<?> items = collection.iterator();
126+
while (items.hasNext()) {
127+
Object item = items.next();
128+
traverse(context.descend(item, new CollectionMatch(items, collection,
129+
item, context.getParentMatch())), matchCallback);
130+
}
131+
}
132+
}
133+
134+
private void handleWildcardPayload(Collection<?> collection,
135+
MatchCallback matchCallback, ProcessingContext context) {
118136
Iterator<?> items = collection.iterator();
119137
if (context.isLeaf()) {
120138
while (items.hasNext()) {
@@ -147,7 +165,7 @@ private void handleMapPayload(ProcessingContext context,
147165
}
148166
}
149167
else if ("*".equals(context.getSegment())) {
150-
handleCollectionPayload(map.values(), matchCallback, context);
168+
handleWildcardPayload(map.values(), matchCallback, context);
151169
}
152170
else {
153171
matchCallback.absent();
@@ -309,6 +327,51 @@ private boolean isCollectionWithEntries(Object object) {
309327

310328
}
311329

330+
private static class LeafCollectionMatch implements Match {
331+
332+
private final Collection<?> collection;
333+
334+
private final Match parent;
335+
336+
public LeafCollectionMatch(Collection<?> collection, Match parent) {
337+
this.collection = collection;
338+
this.parent = parent;
339+
}
340+
341+
@Override
342+
public Collection<?> getValue() {
343+
return this.collection;
344+
}
345+
346+
@Override
347+
public void remove() {
348+
if (containsOnlyScalars(this.collection)) {
349+
this.collection.clear();
350+
if (this.parent != null) {
351+
this.parent.remove();
352+
}
353+
}
354+
}
355+
356+
@Override
357+
public void removeSubsection() {
358+
this.collection.clear();
359+
if (this.parent != null) {
360+
this.parent.removeSubsection();
361+
}
362+
}
363+
364+
private boolean containsOnlyScalars(Collection<?> collection) {
365+
for (Object item : collection) {
366+
if (item instanceof Collection || item instanceof Map) {
367+
return false;
368+
}
369+
}
370+
return true;
371+
}
372+
373+
}
374+
312375
private interface MatchCallback {
313376

314377
void foundMatch(Match match);
@@ -371,4 +434,25 @@ private ProcessingContext descend(Object payload, Match match) {
371434
}
372435
}
373436

437+
static class ExtractedField {
438+
439+
private final Object value;
440+
441+
private final PathType type;
442+
443+
ExtractedField(Object value, PathType type) {
444+
this.value = value;
445+
this.type = type;
446+
}
447+
448+
Object getValue() {
449+
return this.value;
450+
}
451+
452+
PathType getType() {
453+
return this.type;
454+
}
455+
456+
}
457+
374458
}

0 commit comments

Comments
 (0)