Merge branch '1.2.x'
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
* Copyright 2014-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,7 +31,8 @@ public class FieldDoesNotExistException extends RuntimeException {
|
||||
*
|
||||
* @param fieldPath the path of the field that does not exist
|
||||
*/
|
||||
public FieldDoesNotExistException(JsonFieldPath fieldPath) {
|
||||
public FieldDoesNotExistException(String fieldPath) {
|
||||
super("The payload does not contain a field with the path '" + fieldPath + "'");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.List;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.payload.JsonFieldPath.PathType;
|
||||
import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField;
|
||||
|
||||
/**
|
||||
* A {@link PayloadSubsectionExtractor} that extracts the subsection of the JSON payload
|
||||
@@ -66,20 +68,20 @@ public class FieldPathPayloadSubsectionExtractor
|
||||
public byte[] extractSubsection(byte[] payload, MediaType contentType) {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
try {
|
||||
JsonFieldPath compiledPath = JsonFieldPath.compile(this.fieldPath);
|
||||
Object extracted = new JsonFieldProcessor().extract(compiledPath,
|
||||
objectMapper.readValue(payload, Object.class));
|
||||
if (extracted instanceof List && !compiledPath.isPrecise()) {
|
||||
List<?> extractedList = (List<?>) extracted;
|
||||
ExtractedField extractedField = new JsonFieldProcessor().extract(
|
||||
this.fieldPath, objectMapper.readValue(payload, Object.class));
|
||||
Object value = extractedField.getValue();
|
||||
if (value instanceof List && extractedField.getType() == PathType.MULTI) {
|
||||
List<?> extractedList = (List<?>) value;
|
||||
if (extractedList.size() == 1) {
|
||||
extracted = extractedList.get(0);
|
||||
value = extractedList.get(0);
|
||||
}
|
||||
else {
|
||||
throw new PayloadHandlingException(this.fieldPath
|
||||
+ " does not uniquely identify a subsection of the payload");
|
||||
}
|
||||
}
|
||||
return objectMapper.writeValueAsBytes(extracted);
|
||||
return objectMapper.writeValueAsBytes(value);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new PayloadHandlingException(ex);
|
||||
|
||||
@@ -52,8 +52,8 @@ class JsonContentHandler implements ContentHandler {
|
||||
List<FieldDescriptor> missingFields = new ArrayList<>();
|
||||
Object payload = readContent();
|
||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||
if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField(
|
||||
JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) {
|
||||
if (!fieldDescriptor.isOptional() && !this.fieldProcessor
|
||||
.hasField(fieldDescriptor.getPath(), payload)) {
|
||||
missingFields.add(fieldDescriptor);
|
||||
}
|
||||
}
|
||||
@@ -65,12 +65,11 @@ class JsonContentHandler implements ContentHandler {
|
||||
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
|
||||
Object content = readContent();
|
||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||
JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath());
|
||||
if (describesSubsection(fieldDescriptor)) {
|
||||
this.fieldProcessor.removeSubsection(path, content);
|
||||
this.fieldProcessor.removeSubsection(fieldDescriptor.getPath(), content);
|
||||
}
|
||||
else {
|
||||
this.fieldProcessor.remove(path, content);
|
||||
this.fieldProcessor.remove(fieldDescriptor.getPath(), content);
|
||||
}
|
||||
}
|
||||
if (!isEmpty(content)) {
|
||||
@@ -107,7 +106,7 @@ class JsonContentHandler implements ContentHandler {
|
||||
@Override
|
||||
public Object determineFieldType(FieldDescriptor fieldDescriptor) {
|
||||
if (fieldDescriptor.getType() == null) {
|
||||
return this.fieldTypeResolver.resolveFieldType(fieldDescriptor.getPath(),
|
||||
return this.fieldTypeResolver.resolveFieldType(fieldDescriptor,
|
||||
readContent());
|
||||
}
|
||||
if (!(fieldDescriptor.getType() instanceof JsonFieldType)) {
|
||||
@@ -116,7 +115,7 @@ class JsonContentHandler implements ContentHandler {
|
||||
JsonFieldType descriptorFieldType = (JsonFieldType) fieldDescriptor.getType();
|
||||
try {
|
||||
JsonFieldType actualFieldType = this.fieldTypeResolver
|
||||
.resolveFieldType(fieldDescriptor.getPath(), readContent());
|
||||
.resolveFieldType(fieldDescriptor, readContent());
|
||||
if (descriptorFieldType == JsonFieldType.VARIES
|
||||
|| descriptorFieldType == actualFieldType
|
||||
|| (fieldDescriptor.isOptional()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
* Copyright 2014-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +27,6 @@ import java.util.regex.Pattern;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Jeremy Rickard
|
||||
*
|
||||
*/
|
||||
final class JsonFieldPath {
|
||||
|
||||
@@ -41,24 +40,16 @@ final class JsonFieldPath {
|
||||
|
||||
private final List<String> segments;
|
||||
|
||||
private final boolean precise;
|
||||
private final PathType type;
|
||||
|
||||
private final boolean array;
|
||||
|
||||
private JsonFieldPath(String rawPath, List<String> segments, boolean precise,
|
||||
boolean array) {
|
||||
private JsonFieldPath(String rawPath, List<String> segments, PathType type) {
|
||||
this.rawPath = rawPath;
|
||||
this.segments = segments;
|
||||
this.precise = precise;
|
||||
this.array = array;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
boolean isPrecise() {
|
||||
return this.precise;
|
||||
}
|
||||
|
||||
boolean isArray() {
|
||||
return this.array;
|
||||
PathType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
List<String> getSegments() {
|
||||
@@ -72,9 +63,8 @@ final class JsonFieldPath {
|
||||
|
||||
static JsonFieldPath compile(String path) {
|
||||
List<String> segments = extractSegments(path);
|
||||
String leafSegment = segments.get(segments.size() - 1);
|
||||
return new JsonFieldPath(path, segments, matchesSingleValue(segments),
|
||||
isArraySegment(leafSegment) || isWildcardSegment(leafSegment));
|
||||
return new JsonFieldPath(path, segments,
|
||||
matchesSingleValue(segments) ? PathType.SINGLE : PathType.MULTI);
|
||||
}
|
||||
|
||||
static boolean isArraySegment(String segment) {
|
||||
@@ -84,8 +74,9 @@ final class JsonFieldPath {
|
||||
static boolean matchesSingleValue(List<String> segments) {
|
||||
Iterator<String> iterator = segments.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String next = iterator.next();
|
||||
if ((isArraySegment(next) || isWildcardSegment(next)) && iterator.hasNext()) {
|
||||
String segment = iterator.next();
|
||||
if ((isArraySegment(segment) && iterator.hasNext())
|
||||
|| isWildcardSegment(segment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -132,4 +123,19 @@ final class JsonFieldPath {
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
static enum PathType {
|
||||
|
||||
/**
|
||||
* The path identifies a single item in the payload
|
||||
*/
|
||||
SINGLE,
|
||||
|
||||
/**
|
||||
* The path identifies multiple items in the payload
|
||||
*/
|
||||
MULTI;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.restdocs.payload.JsonFieldPath.PathType;
|
||||
|
||||
/**
|
||||
* A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be
|
||||
* extracted and removed.
|
||||
@@ -31,15 +33,16 @@ import java.util.Map;
|
||||
*/
|
||||
final class JsonFieldProcessor {
|
||||
|
||||
boolean hasField(final JsonFieldPath fieldPath, Object payload) {
|
||||
boolean hasField(String path, Object payload) {
|
||||
HasFieldMatchCallback callback = new HasFieldMatchCallback();
|
||||
traverse(new ProcessingContext(payload, fieldPath), callback);
|
||||
traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)), callback);
|
||||
return callback.fieldFound();
|
||||
}
|
||||
|
||||
Object extract(JsonFieldPath path, Object payload) {
|
||||
ExtractedField extract(String path, Object payload) {
|
||||
JsonFieldPath compiledPath = JsonFieldPath.compile(path);
|
||||
final List<Object> matches = new ArrayList<>();
|
||||
traverse(new ProcessingContext(payload, path), new MatchCallback() {
|
||||
traverse(new ProcessingContext(payload, compiledPath), new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
@@ -55,44 +58,43 @@ final class JsonFieldProcessor {
|
||||
if (matches.isEmpty()) {
|
||||
throw new FieldDoesNotExistException(path);
|
||||
}
|
||||
if ((!path.isArray()) && path.isPrecise()) {
|
||||
return matches.get(0);
|
||||
}
|
||||
else {
|
||||
return matches;
|
||||
}
|
||||
return new ExtractedField(
|
||||
compiledPath.getType() == PathType.SINGLE ? matches.get(0) : matches,
|
||||
compiledPath.getType());
|
||||
}
|
||||
|
||||
void remove(final JsonFieldPath path, Object payload) {
|
||||
traverse(new ProcessingContext(payload, path), new MatchCallback() {
|
||||
void remove(String path, Object payload) {
|
||||
traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)),
|
||||
new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
match.remove();
|
||||
}
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
match.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void absent() {
|
||||
@Override
|
||||
public void absent() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void removeSubsection(final JsonFieldPath path, Object payload) {
|
||||
traverse(new ProcessingContext(payload, path), new MatchCallback() {
|
||||
void removeSubsection(String path, Object payload) {
|
||||
traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)),
|
||||
new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
match.removeSubsection();
|
||||
}
|
||||
@Override
|
||||
public void foundMatch(Match match) {
|
||||
match.removeSubsection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void absent() {
|
||||
@Override
|
||||
public void absent() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void traverse(ProcessingContext context, MatchCallback matchCallback) {
|
||||
@@ -115,6 +117,22 @@ final class JsonFieldProcessor {
|
||||
|
||||
private void handleCollectionPayload(Collection<?> collection,
|
||||
MatchCallback matchCallback, ProcessingContext context) {
|
||||
if (context.isLeaf()) {
|
||||
matchCallback.foundMatch(
|
||||
new LeafCollectionMatch(collection, context.getParentMatch()));
|
||||
}
|
||||
else {
|
||||
Iterator<?> items = collection.iterator();
|
||||
while (items.hasNext()) {
|
||||
Object item = items.next();
|
||||
traverse(context.descend(item, new CollectionMatch(items, collection,
|
||||
item, context.getParentMatch())), matchCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleWildcardPayload(Collection<?> collection,
|
||||
MatchCallback matchCallback, ProcessingContext context) {
|
||||
Iterator<?> items = collection.iterator();
|
||||
if (context.isLeaf()) {
|
||||
while (items.hasNext()) {
|
||||
@@ -147,7 +165,7 @@ final class JsonFieldProcessor {
|
||||
}
|
||||
}
|
||||
else if ("*".equals(context.getSegment())) {
|
||||
handleCollectionPayload(map.values(), matchCallback, context);
|
||||
handleWildcardPayload(map.values(), matchCallback, context);
|
||||
}
|
||||
else {
|
||||
matchCallback.absent();
|
||||
@@ -309,6 +327,51 @@ final class JsonFieldProcessor {
|
||||
|
||||
}
|
||||
|
||||
private static class LeafCollectionMatch implements Match {
|
||||
|
||||
private final Collection<?> collection;
|
||||
|
||||
private final Match parent;
|
||||
|
||||
public LeafCollectionMatch(Collection<?> collection, Match parent) {
|
||||
this.collection = collection;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<?> getValue() {
|
||||
return this.collection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (containsOnlyScalars(this.collection)) {
|
||||
this.collection.clear();
|
||||
if (this.parent != null) {
|
||||
this.parent.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSubsection() {
|
||||
this.collection.clear();
|
||||
if (this.parent != null) {
|
||||
this.parent.removeSubsection();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsOnlyScalars(Collection<?> collection) {
|
||||
for (Object item : collection) {
|
||||
if (item instanceof Collection || item instanceof Map) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private interface MatchCallback {
|
||||
|
||||
void foundMatch(Match match);
|
||||
@@ -371,4 +434,25 @@ final class JsonFieldProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
static class ExtractedField {
|
||||
|
||||
private final Object value;
|
||||
|
||||
private final PathType type;
|
||||
|
||||
ExtractedField(Object value, PathType type) {
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
Object getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
PathType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
* Copyright 2014-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,9 @@ package org.springframework.restdocs.payload;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.restdocs.payload.JsonFieldPath.PathType;
|
||||
import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField;
|
||||
|
||||
/**
|
||||
* Resolves the type of a field in a JSON request or response payload.
|
||||
*
|
||||
@@ -28,23 +31,32 @@ class JsonFieldTypeResolver {
|
||||
|
||||
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
|
||||
|
||||
JsonFieldType resolveFieldType(String path, Object payload) {
|
||||
JsonFieldPath fieldPath = JsonFieldPath.compile(path);
|
||||
Object field = this.fieldProcessor.extract(fieldPath, payload);
|
||||
if (field instanceof Collection && !fieldPath.isPrecise()) {
|
||||
JsonFieldType resolveFieldType(FieldDescriptor fieldDescriptor, Object payload) {
|
||||
ExtractedField extractedField = this.fieldProcessor
|
||||
.extract(fieldDescriptor.getPath(), payload);
|
||||
Object value = extractedField.getValue();
|
||||
if (value instanceof Collection && extractedField.getType() == PathType.MULTI) {
|
||||
JsonFieldType commonType = null;
|
||||
for (Object item : (Collection<?>) field) {
|
||||
for (Object item : (Collection<?>) value) {
|
||||
JsonFieldType fieldType = determineFieldType(item);
|
||||
if (commonType == null) {
|
||||
commonType = fieldType;
|
||||
}
|
||||
else if (fieldType != commonType && fieldType != JsonFieldType.NULL) {
|
||||
return JsonFieldType.VARIES;
|
||||
else if (fieldType != commonType) {
|
||||
if (!fieldDescriptor.isOptional()) {
|
||||
return JsonFieldType.VARIES;
|
||||
}
|
||||
if (commonType == JsonFieldType.NULL) {
|
||||
commonType = fieldType;
|
||||
}
|
||||
else if (fieldType != JsonFieldType.NULL) {
|
||||
return JsonFieldType.VARIES;
|
||||
}
|
||||
}
|
||||
}
|
||||
return commonType;
|
||||
}
|
||||
return determineFieldType(field);
|
||||
return determineFieldType(value);
|
||||
}
|
||||
|
||||
private JsonFieldType determineFieldType(Object fieldValue) {
|
||||
|
||||
Reference in New Issue
Block a user