Merge branch '1.2.x'

This commit is contained in:
Andy Wilkinson
2017-09-23 16:41:05 +01:00
10 changed files with 427 additions and 197 deletions

View File

@@ -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 + "'");
}
}

View File

@@ -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);

View File

@@ -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()

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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) {